@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 +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 +33 -7
- package/lib/compression/log-compressor.js +4 -4
- package/lib/index.js +8 -5
- package/lib/tokens/tracker.js +32 -4
- package/package.json +1 -1
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)
|
|
@@ -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:
|
|
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
|
-
|
|
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) => {
|
|
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
|
|
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
|
@@ -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
|
}
|
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 };
|