@goodandready/dsh-context-lens 0.1.5 → 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 +112 -24
- package/README.ru.md +120 -0
- package/README.zh.md +73 -0
- package/lib/ast/skeletonizer.js +67 -8
- package/lib/client.js +92 -5
- package/lib/compression/log-compressor.js +4 -4
- package/lib/index.js +32 -11
- package/lib/tokens/tracker.js +32 -4
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -1,35 +1,123 @@
|
|
|
1
|
-
# dsh-context-lens
|
|
1
|
+
# 📦 @goodandready/dsh-context-lens
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<div align="center">
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
<h3>Intelligent AST Code Skeletonizer, Context Token Compressor & Log Condenser for DeepSeek Harness</h3>
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
-
|
|
9
|
-
|
|
10
|
-
-
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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)
|
package/lib/ast/skeletonizer.js
CHANGED
|
@@ -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
|
-
|
|
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)
|
|
@@ -48,10 +54,51 @@ window.__ModuleLoader__.load({
|
|
|
48
54
|
return lines.filter((_, i) => keep[i]).slice(0, 200).join('\n');
|
|
49
55
|
}
|
|
50
56
|
|
|
57
|
+
function LensTab({ ctx: _ctx, scope }) {
|
|
58
|
+
const [stats, setStats] = React.useState(null);
|
|
59
|
+
const [history, setHistory] = React.useState([]);
|
|
60
|
+
React.useEffect(() => {
|
|
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(() => {});
|
|
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)';
|
|
65
|
+
return React.createElement('div', { style: { padding: 12 } },
|
|
66
|
+
React.createElement('div', { style: { fontSize: 13, fontWeight: 600, marginBottom: 8, color: 'var(--dsw-alias-label-primary)' } }, 'Context Lens'),
|
|
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,
|
|
78
|
+
React.createElement('div', { style: { marginTop: 12, display: 'flex', gap: 8 } },
|
|
79
|
+
React.createElement('button', { onClick: refresh, style: { fontSize: 12, padding: '4px 8px', borderRadius: 6, border: '1px solid var(--dsw-alias-border-l2)', background: 'var(--dsw-alias-bg-layer-3)', cursor: 'pointer' } }, 'Refresh'),
|
|
80
|
+
React.createElement('button', { onClick: () => { try { _ctx.betterSidebar.openTab({ type: 'dsh-context-lens:tab', title: 'Lens' }) } catch(e){} }, style: { fontSize: 12, padding: '4px 8px', borderRadius: 6, border: '1px solid var(--dsw-alias-border-l2)', background: 'transparent', cursor: 'pointer' } }, 'Settings')
|
|
81
|
+
)
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function HeaderBadge({ ctx: _ctx }) {
|
|
86
|
+
const [stats, setStats] = React.useState(null);
|
|
87
|
+
React.useEffect(() => {
|
|
88
|
+
const tick = () => fetch('/dsh-context-lens/status').then(r => r.ok ? r.json() : null).then(j => { if (j && j.stats) setStats(j.stats); }).catch(() => {});
|
|
89
|
+
tick();
|
|
90
|
+
const id = setInterval(tick, 5000);
|
|
91
|
+
return () => clearInterval(id);
|
|
92
|
+
}, []);
|
|
93
|
+
if (!stats || !stats.savedTokens) return null;
|
|
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 ? ' ⚠' : ''}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
51
98
|
function PluginCard({ ctx: _ctx, t }) {
|
|
52
99
|
const [expanded, setExpanded] = React.useState(false);
|
|
53
100
|
// hooks must be before any return — React 310
|
|
54
|
-
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 });
|
|
55
102
|
const [status, setStatus] = React.useState('loading');
|
|
56
103
|
const [saving, setSaving] = React.useState(false);
|
|
57
104
|
const [saveErr, setSaveErr] = React.useState('');
|
|
@@ -92,7 +139,13 @@ window.__ModuleLoader__.load({
|
|
|
92
139
|
|
|
93
140
|
React.useEffect(() => {
|
|
94
141
|
if (expanded) {
|
|
95
|
-
fetch('/dsh-context-lens/status').then((r) => r.ok ? r.json() : null).then((j) => {
|
|
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(() => {});
|
|
96
149
|
}
|
|
97
150
|
}, [expanded]);
|
|
98
151
|
|
|
@@ -206,7 +259,7 @@ window.__ModuleLoader__.load({
|
|
|
206
259
|
);
|
|
207
260
|
}
|
|
208
261
|
|
|
209
|
-
module.exports.inject = ['slots', 'locale'];
|
|
262
|
+
module.exports.inject = ['slots', 'locale', 'betterSidebar'];
|
|
210
263
|
module.exports.apply = function apply(ctx) {
|
|
211
264
|
try { ctx.locale.register(NS, { en, ru }); } catch (e) { console.warn('[dsh-context-lens] locale register failed', e && e.message || e); }
|
|
212
265
|
if (!ctx.slots) return;
|
|
@@ -242,6 +295,40 @@ window.__ModuleLoader__.load({
|
|
|
242
295
|
} else {
|
|
243
296
|
try { doRegister(); } catch (e) { console.error('[dsh-context-lens] direct register failed (no inject)', e && e.stack || e); throw e; }
|
|
244
297
|
}
|
|
298
|
+
// BetterSidebar tab (optional, for dsh-better-sidebar)
|
|
299
|
+
if (ctx.betterSidebar && typeof ctx.betterSidebar.registerTab === 'function') {
|
|
300
|
+
try {
|
|
301
|
+
ctx.effect(() => ctx.betterSidebar.registerTab({
|
|
302
|
+
id: 'dsh-context-lens:tab',
|
|
303
|
+
title: () => 'Lens',
|
|
304
|
+
icon: () => React.createElement('span', null, '◐'),
|
|
305
|
+
order: 50,
|
|
306
|
+
component: ({ scope }) => React.createElement(LensTab, { ctx, scope })
|
|
307
|
+
}));
|
|
308
|
+
} catch (e) {
|
|
309
|
+
console.warn('[dsh-context-lens] betterSidebar registerTab failed', e && e.message || e);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// Header badge (#20) — compact savings in conversation header
|
|
313
|
+
if (ctx.slots) {
|
|
314
|
+
const headerBadgeRegister = () => {
|
|
315
|
+
try {
|
|
316
|
+
return ctx.slots.register({
|
|
317
|
+
name: 'conversation.header',
|
|
318
|
+
id: 'dsh-context-lens:header-badge',
|
|
319
|
+
order: 50,
|
|
320
|
+
inject: () => ({ ctx })
|
|
321
|
+
}, HeaderBadge);
|
|
322
|
+
} catch (e) {
|
|
323
|
+
console.warn('[dsh-context-lens] header badge register failed', e && e.message || e);
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
if (typeof ctx.slots.inject === 'function') {
|
|
327
|
+
try { ctx.slots.inject('conversation.header', headerBadgeRegister); } catch (e) { console.warn('[dsh-context-lens] header inject failed', e && e.message || e); try { headerBadgeRegister(); } catch (e2) {} }
|
|
328
|
+
} else {
|
|
329
|
+
try { headerBadgeRegister(); } catch (e) {}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
245
332
|
};
|
|
246
333
|
return module.exports;
|
|
247
334
|
}
|
|
@@ -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
|
|
4
|
-
const NOISE_RE =
|
|
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
|
@@ -9,7 +9,10 @@ export const inject = ['tools', 'settings', 'webServer'];
|
|
|
9
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
|
-
tokenSavingsTracking: z.boolean().default(true).description('Track and display token budget savings')
|
|
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)'),
|
|
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')
|
|
13
16
|
});
|
|
14
17
|
|
|
15
18
|
const NS = '@goodandready/dsh-context-lens';
|
|
@@ -20,6 +23,11 @@ let focusState = { paths: [], updatedAt: null };
|
|
|
20
23
|
const OUTPUT_SCHEMA = { type: 'object', properties: { success: { type: 'boolean' } }, additionalProperties: true };
|
|
21
24
|
const renderOutput = (_args, result) => JSON.stringify(result, null, 2);
|
|
22
25
|
|
|
26
|
+
function shouldAutoCompress(text, threshold) {
|
|
27
|
+
if (!threshold || threshold <= 0) return false;
|
|
28
|
+
return (text || '').length > threshold || (text || '').split('\n').length > 100;
|
|
29
|
+
}
|
|
30
|
+
|
|
23
31
|
export function apply(ctx, config) {
|
|
24
32
|
let getConfig = () => config;
|
|
25
33
|
|
|
@@ -65,7 +73,8 @@ export function apply(ctx, config) {
|
|
|
65
73
|
properties: {
|
|
66
74
|
text: { type: 'string', description: 'Raw log text to compress' },
|
|
67
75
|
mode: { type: 'string', enum: ['raw', 'balanced', 'aggressive'], description: 'Compression aggressiveness' },
|
|
68
|
-
maxLines: { type: 'number', description: 'Max output lines' }
|
|
76
|
+
maxLines: { type: 'number', description: 'Max output lines' },
|
|
77
|
+
auto: { type: 'boolean', description: 'Auto-compress if large (uses threshold)' }
|
|
69
78
|
},
|
|
70
79
|
required: ['text']
|
|
71
80
|
},
|
|
@@ -74,21 +83,26 @@ export function apply(ctx, config) {
|
|
|
74
83
|
const cfg = getConfig();
|
|
75
84
|
const mode = params.mode || cfg.compressionMode || 'balanced';
|
|
76
85
|
const maxLines = params.maxLines || 400;
|
|
77
|
-
|
|
86
|
+
// Auto-compress check (#15)
|
|
87
|
+
const threshold = cfg.autoCompressThreshold ?? 4000;
|
|
88
|
+
const useAuto = params.auto !== false && shouldAutoCompress(params.text, threshold);
|
|
89
|
+
const effectiveMode = useAuto ? mode : mode;
|
|
90
|
+
const res = compressLog(params.text, { mode: effectiveMode, maxLines });
|
|
78
91
|
maybeTrack(params.text, res.compressed);
|
|
79
|
-
return { success: true, mode, ...res };
|
|
92
|
+
return { success: true, mode: effectiveMode, autoCompressed: useAuto, ...res };
|
|
80
93
|
}
|
|
81
94
|
});
|
|
82
95
|
|
|
83
96
|
ctx.tools.register({
|
|
84
97
|
name: 'context_lens_compress_code',
|
|
85
|
-
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',
|
|
86
99
|
parameters: {
|
|
87
100
|
type: 'object',
|
|
88
101
|
properties: {
|
|
89
102
|
code: { type: 'string', description: 'Source code to skeletonize' },
|
|
90
|
-
language: { type: 'string', enum: ['js', 'ts', 'py', 'python', 'go'], description: 'Language hint' },
|
|
91
|
-
maxDepth: { type: 'number', description: 'Max depth' }
|
|
103
|
+
language: { type: 'string', enum: ['js', 'ts', 'py', 'python', 'go', 'rust', 'java'], description: 'Language hint' },
|
|
104
|
+
maxDepth: { type: 'number', description: 'Max depth' },
|
|
105
|
+
filePath: { type: 'string', description: 'File path to check focus (if in focus, returns full code)' }
|
|
92
106
|
},
|
|
93
107
|
required: ['code']
|
|
94
108
|
},
|
|
@@ -96,9 +110,15 @@ export function apply(ctx, config) {
|
|
|
96
110
|
execute: async (params) => {
|
|
97
111
|
const cfg = getConfig();
|
|
98
112
|
const maxDepth = params.maxDepth ?? cfg.astSkeletonMaxDepth ?? 3;
|
|
113
|
+
// Focus mode (#18): if file is in focus, return full code, else skeleton
|
|
114
|
+
const isFocused = params.filePath && focusState.paths.length > 0 ? focusState.paths.some(p => params.filePath.includes(p) || p.includes(params.filePath)) : false;
|
|
115
|
+
if (isFocused) {
|
|
116
|
+
maybeTrack(params.code, params.code);
|
|
117
|
+
return { success: true, skeleton: params.code, originalTokens: estLog(params.code), skeletonTokens: estLog(params.code), maxDepth, focused: true, hint: 'File is in focus, returned full code' };
|
|
118
|
+
}
|
|
99
119
|
const skeleton = skeletonize(params.code, { maxDepth, language: params.language });
|
|
100
120
|
maybeTrack(params.code, skeleton);
|
|
101
|
-
return { success: true, skeleton, originalTokens: estLog(params.code), skeletonTokens: estLog(skeleton), maxDepth };
|
|
121
|
+
return { success: true, skeleton, originalTokens: estLog(params.code), skeletonTokens: estLog(skeleton), maxDepth, focused: false };
|
|
102
122
|
}
|
|
103
123
|
});
|
|
104
124
|
|
|
@@ -109,8 +129,9 @@ export function apply(ctx, config) {
|
|
|
109
129
|
output: { schema: OUTPUT_SCHEMA, render: renderOutput },
|
|
110
130
|
execute: async () => {
|
|
111
131
|
const stats = tracker.getStats();
|
|
132
|
+
const history = tracker.getHistory();
|
|
112
133
|
const cfg = getConfig();
|
|
113
|
-
return { success: true, ...stats, trackingEnabled: cfg.tokenSavingsTracking !== false, focus: focusState };
|
|
134
|
+
return { success: true, ...stats, history, trackingEnabled: cfg.tokenSavingsTracking !== false, autoCollapse: cfg.autoCollapse !== false, focus: focusState, autoCompressThreshold: cfg.autoCompressThreshold };
|
|
114
135
|
}
|
|
115
136
|
});
|
|
116
137
|
}
|
|
@@ -122,10 +143,10 @@ export function apply(ctx, config) {
|
|
|
122
143
|
path: '/dsh-context-lens/status',
|
|
123
144
|
handler: (req, res) => {
|
|
124
145
|
res.setHeader('content-type', 'application/json');
|
|
125
|
-
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 }));
|
|
126
147
|
}
|
|
127
148
|
}), 'dsh-context-lens status route');
|
|
128
149
|
}
|
|
129
150
|
}
|
|
130
151
|
|
|
131
|
-
export { compressLog, skeletonize };
|
|
152
|
+
export { compressLog, skeletonize, shouldAutoCompress };
|
package/lib/tokens/tracker.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|
|
@@ -55,6 +55,12 @@
|
|
|
55
55
|
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
|
|
56
56
|
"@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
|
|
57
57
|
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
|
|
58
|
-
"@deepseek-ai/schemastery": "^3.18.1"
|
|
58
|
+
"@deepseek-ai/schemastery": "^3.18.1",
|
|
59
|
+
"dsh-better-sidebar": "^0.18.0-alpha.0"
|
|
60
|
+
},
|
|
61
|
+
"peerDependenciesMeta": {
|
|
62
|
+
"dsh-better-sidebar": {
|
|
63
|
+
"optional": true
|
|
64
|
+
}
|
|
59
65
|
}
|
|
60
66
|
}
|