@goodandready/dsh-context-lens 0.1.24 → 0.1.26
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/CHANGELOG.md +14 -0
- package/README.md +47 -96
- package/README.ru.md +39 -113
- package/README.zh.md +42 -116
- package/lib/auto-compress.js +23 -4
- package/lib/client.js +356 -891
- package/lib/compression/log-compressor.js +53 -28
- package/lib/index.js +58 -173
- package/lib/tools.js +99 -113
- package/package.json +5 -12
- package/lib/http.js +0 -114
- package/lib/tokens/estimate.js +0 -6
- package/lib/tokens/tracker.js +0 -89
- package/lib/updater.js +0 -237
package/lib/tools.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { compressLog, estimateTokens as estLog } from './compression/log-compressor.js';
|
|
2
1
|
import { skeletonize } from './ast/skeletonizer.js';
|
|
3
|
-
import
|
|
2
|
+
import { compressLog } from './compression/log-compressor.js';
|
|
4
3
|
import { shouldAutoCompress } from './auto-compress.js';
|
|
5
4
|
|
|
6
|
-
function normalizePath(p) {
|
|
5
|
+
export function normalizePath(p) {
|
|
7
6
|
return (p || '').replace(/\\/g, '/').replace(/\/+$/, '');
|
|
8
7
|
}
|
|
9
8
|
|
|
@@ -17,7 +16,11 @@ export function isPathFocused(pattern, filePath) {
|
|
|
17
16
|
return false;
|
|
18
17
|
}
|
|
19
18
|
|
|
20
|
-
export const OUTPUT_SCHEMA = {
|
|
19
|
+
export const OUTPUT_SCHEMA = {
|
|
20
|
+
type: 'object',
|
|
21
|
+
properties: { success: { type: 'boolean' } },
|
|
22
|
+
additionalProperties: true
|
|
23
|
+
};
|
|
21
24
|
|
|
22
25
|
// #71 (GH #2): output.render MUST return ContentBlock[] [{ type: 'text', text: ... }]
|
|
23
26
|
export const renderOutput = (_args, result) => [
|
|
@@ -67,148 +70,131 @@ export function getFocusBySessionSize() {
|
|
|
67
70
|
return focusBySession.size;
|
|
68
71
|
}
|
|
69
72
|
|
|
70
|
-
export function
|
|
71
|
-
return lastActiveSession;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function registerTools(ctx, { getConfig, maybeTrack }) {
|
|
73
|
+
export function registerTools(ctx, { getConfig } = {}) {
|
|
75
74
|
if (!ctx?.tools?.register) return;
|
|
76
75
|
|
|
76
|
+
// Tool 1: Code semantic compression & focus management
|
|
77
77
|
ctx.tools.register({
|
|
78
|
-
name: '
|
|
79
|
-
description: '
|
|
78
|
+
name: 'context_lens_code',
|
|
79
|
+
description: 'Semantic AST skeletonization of code (JS/TS/Python/Go/Rust/Java/C/C++/SQL) and path-based focus management',
|
|
80
80
|
parameters: {
|
|
81
81
|
type: 'object',
|
|
82
82
|
properties: {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
action: {
|
|
84
|
+
type: 'string',
|
|
85
|
+
enum: ['skeleton', 'focus', 'get_focus', 'clear_focus'],
|
|
86
|
+
description: "Action to perform: 'skeleton' (default: AST code skeleton), 'focus' (set session focus paths), 'get_focus' (view active focus), or 'clear_focus' (reset focus)"
|
|
87
|
+
},
|
|
88
|
+
code: { type: 'string', description: 'Source code to skeletonize (for action=skeleton)' },
|
|
89
|
+
language: { type: 'string', enum: ['js', 'ts', 'py', 'python', 'go', 'rust', 'java', 'c', 'cpp', 'sql'], description: 'Language hint for skeletonization' },
|
|
90
|
+
maxDepth: { type: 'number', description: 'Max depth for AST skeleton' },
|
|
91
|
+
filePath: { type: 'string', description: 'File path to check against focus paths (if in focus, returns full code without skeletonization)' },
|
|
92
|
+
paths: { type: 'array', items: { type: 'string' }, description: 'File paths or patterns to put in focus (for action=focus)' },
|
|
93
|
+
sessionId: { type: 'string', description: 'Session id for session-isolated focus' }
|
|
94
|
+
}
|
|
87
95
|
},
|
|
88
96
|
output: { schema: OUTPUT_SCHEMA, render: renderOutput },
|
|
89
97
|
execute: async (params, meta) => {
|
|
98
|
+
const action = (params && params.action) || 'skeleton';
|
|
90
99
|
const key = sessionKey(params, meta);
|
|
91
|
-
const rawPaths = Array.isArray(params.paths) ? params.paths : [];
|
|
92
|
-
const paths = rawPaths
|
|
93
|
-
.filter(p => typeof p === 'string')
|
|
94
|
-
.map(p => p.trim())
|
|
95
|
-
.filter(p => p.length > 0);
|
|
96
|
-
const state = getFocus(key);
|
|
97
|
-
state.paths = paths;
|
|
98
|
-
state.updatedAt = new Date().toISOString();
|
|
99
|
-
return { success: true, sessionId: key, focus: state };
|
|
100
|
-
}
|
|
101
|
-
});
|
|
102
100
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
auto: { type: 'boolean', description: 'Auto-compress if large (uses threshold); keeps configured compressionMode' }
|
|
101
|
+
if (action === 'focus') {
|
|
102
|
+
const rawPaths = Array.isArray(params?.paths) ? params.paths : [];
|
|
103
|
+
const paths = rawPaths
|
|
104
|
+
.filter(p => typeof p === 'string')
|
|
105
|
+
.map(p => p.trim())
|
|
106
|
+
.filter(p => p.length > 0);
|
|
107
|
+
const state = getFocus(key);
|
|
108
|
+
state.paths = paths;
|
|
109
|
+
state.updatedAt = new Date().toISOString();
|
|
110
|
+
return { success: true, action: 'focus', sessionId: key, focus: state };
|
|
114
111
|
}
|
|
115
|
-
},
|
|
116
|
-
output: { schema: OUTPUT_SCHEMA, render: renderOutput },
|
|
117
|
-
execute: async (params) => {
|
|
118
|
-
const cfg = getConfig();
|
|
119
|
-
const inputText = params.text || params.log || '';
|
|
120
|
-
const mode = params.mode || cfg.compressionMode || 'balanced';
|
|
121
|
-
const maxLines = params.maxLines || 400;
|
|
122
|
-
const threshold = cfg.autoCompressThreshold ?? 4000;
|
|
123
|
-
const useAuto = params.auto !== false && shouldAutoCompress(inputText, threshold);
|
|
124
|
-
const effectiveMode = mode;
|
|
125
|
-
const res = compressLog(inputText, { mode: effectiveMode, maxLines });
|
|
126
|
-
maybeTrack(inputText, res.compressed);
|
|
127
|
-
return { success: true, mode: effectiveMode, autoCompressed: useAuto, ...res };
|
|
128
|
-
}
|
|
129
|
-
});
|
|
130
112
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
output: { schema: OUTPUT_SCHEMA, render: renderOutput },
|
|
146
|
-
execute: async (params, meta) => {
|
|
147
|
-
const cfg = getConfig();
|
|
148
|
-
const maxDepth = params.maxDepth ?? cfg.astSkeletonMaxDepth ?? 3;
|
|
149
|
-
const key = sessionKey(params, meta);
|
|
113
|
+
if (action === 'get_focus') {
|
|
114
|
+
return { success: true, action: 'get_focus', sessionId: key, focus: getFocus(key) };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (action === 'clear_focus') {
|
|
118
|
+
clearFocus(key);
|
|
119
|
+
return { success: true, action: 'clear_focus', sessionId: key, focus: getFocus(key) };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Default: skeleton
|
|
123
|
+
const cfg = getConfig ? getConfig() : {};
|
|
124
|
+
const maxDepth = params?.maxDepth ?? cfg.astSkeletonMaxDepth ?? 3;
|
|
125
|
+
const code = params?.code || '';
|
|
126
|
+
const rawFilePath = typeof params?.filePath === 'string' ? params.filePath.trim() : '';
|
|
150
127
|
const focusState = getFocus(key);
|
|
151
|
-
const rawFilePath = typeof params.filePath === 'string' ? params.filePath.trim() : '';
|
|
152
128
|
const isFocused = rawFilePath && focusState.paths.length > 0
|
|
153
129
|
? focusState.paths.some(p => isPathFocused(p, rawFilePath))
|
|
154
130
|
: false;
|
|
131
|
+
|
|
155
132
|
if (isFocused) {
|
|
156
|
-
|
|
157
|
-
|
|
133
|
+
return {
|
|
134
|
+
success: true,
|
|
135
|
+
action: 'skeleton',
|
|
136
|
+
skeleton: code,
|
|
137
|
+
maxDepth,
|
|
138
|
+
focused: true,
|
|
139
|
+
sessionId: key,
|
|
140
|
+
hint: 'File is in focus, returned full code'
|
|
141
|
+
};
|
|
158
142
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
return {
|
|
143
|
+
|
|
144
|
+
const skeleton = skeletonize(code, { maxDepth, language: params?.language });
|
|
145
|
+
return {
|
|
146
|
+
success: true,
|
|
147
|
+
action: 'skeleton',
|
|
148
|
+
skeleton,
|
|
149
|
+
maxDepth,
|
|
150
|
+
focused: false,
|
|
151
|
+
sessionId: key
|
|
152
|
+
};
|
|
162
153
|
}
|
|
163
154
|
});
|
|
164
155
|
|
|
156
|
+
// Tool 2: Test & build log compression
|
|
165
157
|
ctx.tools.register({
|
|
166
|
-
name: '
|
|
167
|
-
description: '
|
|
158
|
+
name: 'context_lens_log',
|
|
159
|
+
description: 'Compress test and build output logs, keeping failures, stack traces, and summary',
|
|
168
160
|
parameters: {
|
|
169
161
|
type: 'object',
|
|
170
162
|
properties: {
|
|
171
|
-
|
|
163
|
+
text: { type: 'string', description: 'Raw log text to compress' },
|
|
164
|
+
log: { type: 'string', description: 'Alias for text' },
|
|
165
|
+
mode: { type: 'string', enum: ['raw', 'balanced', 'aggressive'], description: 'Compression mode (default: balanced)' },
|
|
166
|
+
maxLines: { type: 'number', description: 'Max output lines' },
|
|
167
|
+
auto: { type: 'boolean', description: 'Auto-compress only if large and matches test/build signatures' },
|
|
168
|
+
command: { type: 'string', description: 'Command that generated output (used to identify test/build logs in auto mode)' }
|
|
172
169
|
}
|
|
173
170
|
},
|
|
174
171
|
output: { schema: OUTPUT_SCHEMA, render: renderOutput },
|
|
175
|
-
execute: async (params
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
return { success: true, message: 'Context lens stats and history reset', sessionId: key };
|
|
182
|
-
}
|
|
183
|
-
});
|
|
172
|
+
execute: async (params) => {
|
|
173
|
+
const cfg = getConfig ? getConfig() : {};
|
|
174
|
+
const inputText = params?.text || params?.log || '';
|
|
175
|
+
const mode = params?.mode || cfg.compressionMode || 'balanced';
|
|
176
|
+
const maxLines = params?.maxLines || 400;
|
|
177
|
+
const threshold = cfg.autoCompressThreshold ?? 4000;
|
|
184
178
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
179
|
+
if (params?.auto === true) {
|
|
180
|
+
if (!shouldAutoCompress(inputText, threshold, params?.command)) {
|
|
181
|
+
return {
|
|
182
|
+
success: true,
|
|
183
|
+
mode,
|
|
184
|
+
autoCompressed: false,
|
|
185
|
+
compressed: inputText,
|
|
186
|
+
skipped: true,
|
|
187
|
+
reason: 'Not recognized as test/build output or below threshold; preserved for core pruner'
|
|
188
|
+
};
|
|
189
|
+
}
|
|
192
190
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
execute: async (params, meta) => {
|
|
196
|
-
const cfg = getConfig();
|
|
197
|
-
if (typeof cfg.budgetLimit === 'number') tracker.setBudgetLimit(cfg.budgetLimit);
|
|
198
|
-
if (typeof cfg.budgetAlertPercent === 'number') tracker.setBudgetAlertPercent(cfg.budgetAlertPercent);
|
|
199
|
-
const stats = tracker.getStats(cfg.budgetLimit, cfg.budgetAlertPercent);
|
|
200
|
-
const history = tracker.getHistory();
|
|
201
|
-
const key = sessionKey(params || {}, meta);
|
|
191
|
+
|
|
192
|
+
const res = compressLog(inputText, { mode, maxLines });
|
|
202
193
|
return {
|
|
203
194
|
success: true,
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
autoCollapse: cfg.autoCollapse !== false,
|
|
208
|
-
focus: getFocus(key),
|
|
209
|
-
sessionId: key,
|
|
210
|
-
autoCompressThreshold: cfg.autoCompressThreshold,
|
|
211
|
-
budgetAlertPercent: cfg.budgetAlertPercent ?? 90
|
|
195
|
+
mode,
|
|
196
|
+
autoCompressed: params?.auto === true,
|
|
197
|
+
...res
|
|
212
198
|
};
|
|
213
199
|
}
|
|
214
200
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-context-lens",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "DSH plugin for AST
|
|
3
|
+
"version": "0.1.26",
|
|
4
|
+
"description": "DSH plugin for semantic AST code skeletonization and test/build log compression",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
"dsh",
|
|
27
27
|
"dsh-plugin",
|
|
28
28
|
"context",
|
|
29
|
-
"token-guard",
|
|
30
29
|
"ast-compression",
|
|
31
|
-
"log-
|
|
30
|
+
"log-compression",
|
|
31
|
+
"semantic-compression"
|
|
32
32
|
],
|
|
33
33
|
"repository": {
|
|
34
34
|
"type": "git",
|
|
@@ -54,15 +54,8 @@
|
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
56
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
57
|
-
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
|
|
58
57
|
"@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
|
|
59
58
|
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
|
|
60
|
-
"@deepseek-ai/schemastery": "^3.18.1"
|
|
61
|
-
"dsh-better-sidebar": "^0.18.0-alpha.0"
|
|
62
|
-
},
|
|
63
|
-
"peerDependenciesMeta": {
|
|
64
|
-
"dsh-better-sidebar": {
|
|
65
|
-
"optional": true
|
|
66
|
-
}
|
|
59
|
+
"@deepseek-ai/schemastery": "^3.18.1"
|
|
67
60
|
}
|
|
68
61
|
}
|
package/lib/http.js
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
export function writeJson(res, code, body) {
|
|
2
|
-
try {
|
|
3
|
-
res.writeHead(code, {
|
|
4
|
-
'Content-Type': 'application/json; charset=utf-8',
|
|
5
|
-
'Cache-Control': 'no-store',
|
|
6
|
-
});
|
|
7
|
-
res.end(JSON.stringify(body));
|
|
8
|
-
} catch {
|
|
9
|
-
/* socket already closed */
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function readBody(req, maxBytes = 256 * 1024) {
|
|
14
|
-
return new Promise((resolve, reject) => {
|
|
15
|
-
const chunks = [];
|
|
16
|
-
let size = 0;
|
|
17
|
-
req.on('data', (c) => {
|
|
18
|
-
size += c.length;
|
|
19
|
-
if (size > maxBytes) {
|
|
20
|
-
req.destroy();
|
|
21
|
-
reject(new Error('body too large'));
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
chunks.push(c);
|
|
25
|
-
});
|
|
26
|
-
req.on('end', () => resolve(Buffer.concat(chunks)));
|
|
27
|
-
req.on('error', reject);
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function header(request, name) {
|
|
32
|
-
const value = request?.headers?.[name.toLowerCase()];
|
|
33
|
-
return Array.isArray(value) ? value[0] : value;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function isLoopbackAddress(value) {
|
|
37
|
-
const address = String(value || '').toLowerCase().replace(/^\[|\]$/g, '');
|
|
38
|
-
return address === 'localhost' || address === 'localhost.' || address === '::1'
|
|
39
|
-
|| address.startsWith('127.')
|
|
40
|
-
|| address.startsWith('::ffff:127.');
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** Reject cross-site writes while allowing local, LAN, and reverse-proxy UIs. */
|
|
44
|
-
export function isTrustedRequest(request) {
|
|
45
|
-
const secFetchSite = header(request, 'sec-fetch-site');
|
|
46
|
-
if (secFetchSite === 'cross-site') {
|
|
47
|
-
return false;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const host = header(request, 'x-forwarded-host') || header(request, 'host');
|
|
51
|
-
const origin = header(request, 'origin');
|
|
52
|
-
if (origin) {
|
|
53
|
-
try {
|
|
54
|
-
const url = new URL(origin);
|
|
55
|
-
if (host && url.host.toLowerCase() === host.toLowerCase()) {
|
|
56
|
-
return true;
|
|
57
|
-
}
|
|
58
|
-
if (isLoopbackAddress(url.hostname) && isLoopbackAddress(request?.socket?.remoteAddress)) {
|
|
59
|
-
return true;
|
|
60
|
-
}
|
|
61
|
-
return false;
|
|
62
|
-
} catch {
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const referer = header(request, 'referer');
|
|
68
|
-
if (referer) {
|
|
69
|
-
try {
|
|
70
|
-
const url = new URL(referer);
|
|
71
|
-
if (host && url.host.toLowerCase() === host.toLowerCase()) {
|
|
72
|
-
return true;
|
|
73
|
-
}
|
|
74
|
-
if (isLoopbackAddress(url.hostname) && isLoopbackAddress(request?.socket?.remoteAddress)) {
|
|
75
|
-
return true;
|
|
76
|
-
}
|
|
77
|
-
return false;
|
|
78
|
-
} catch {
|
|
79
|
-
return false;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Requests without origin/referer (e.g. curl, loopback tests, or internal calls)
|
|
84
|
-
if (isLoopbackAddress(request?.socket?.remoteAddress)) {
|
|
85
|
-
return true;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (secFetchSite === 'same-origin' || secFetchSite === 'same-site') {
|
|
89
|
-
return true;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return false;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export const UPDATE_HEADER = 'x-dsh-plugin-update';
|
|
96
|
-
export const ALT_UPDATE_HEADER = 'x-dsh-context-lens-update';
|
|
97
|
-
|
|
98
|
-
export function isTrustedUpdateRequest(request) {
|
|
99
|
-
const updHdr = header(request, UPDATE_HEADER) || header(request, ALT_UPDATE_HEADER);
|
|
100
|
-
if (updHdr !== '1') return false;
|
|
101
|
-
if (!isLoopbackAddress(request?.socket?.remoteAddress)) return false;
|
|
102
|
-
const site = header(request, 'sec-fetch-site');
|
|
103
|
-
if (site !== undefined && site !== 'same-origin') return false;
|
|
104
|
-
const origin = header(request, 'origin');
|
|
105
|
-
const host = header(request, 'host');
|
|
106
|
-
if (origin === undefined || host === undefined) return false;
|
|
107
|
-
try {
|
|
108
|
-
const url = new URL(origin);
|
|
109
|
-
return (url.protocol === 'http:' || url.protocol === 'https:')
|
|
110
|
-
&& isLoopbackAddress(url.hostname) && url.host === host;
|
|
111
|
-
} catch {
|
|
112
|
-
return false;
|
|
113
|
-
}
|
|
114
|
-
}
|
package/lib/tokens/estimate.js
DELETED
package/lib/tokens/tracker.js
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
// ponytail: in-memory per-process tracker + history, no DB
|
|
2
|
-
import { estimateTokens } from './estimate.js';
|
|
3
|
-
|
|
4
|
-
let totalOriginal = 0;
|
|
5
|
-
let totalCompressed = 0;
|
|
6
|
-
let calls = 0;
|
|
7
|
-
const history = []; // last 10
|
|
8
|
-
const MAX_HISTORY = 10;
|
|
9
|
-
let defaultBudgetLimit = 100000; // tokens
|
|
10
|
-
let defaultBudgetAlertPercent = 90; // threshold percent for low budget warning
|
|
11
|
-
|
|
12
|
-
export { estimateTokens };
|
|
13
|
-
|
|
14
|
-
export function setBudgetLimit(limit) {
|
|
15
|
-
if (typeof limit === 'number' && limit > 0) {
|
|
16
|
-
defaultBudgetLimit = limit;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function setBudgetAlertPercent(percent) {
|
|
21
|
-
if (typeof percent === 'number' && percent >= 50 && percent <= 99) {
|
|
22
|
-
defaultBudgetAlertPercent = percent;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function record(originalText, compressedText) {
|
|
27
|
-
const o = estimateTokens(originalText);
|
|
28
|
-
const c = estimateTokens(compressedText);
|
|
29
|
-
const saved = Math.max(0, o - c);
|
|
30
|
-
// #34: stop counting after budgetLimit (description promised this)
|
|
31
|
-
if (totalCompressed >= defaultBudgetLimit) {
|
|
32
|
-
return { originalTokens: o, compressedTokens: c, savedTokens: 0, stopped: true };
|
|
33
|
-
}
|
|
34
|
-
totalOriginal += o;
|
|
35
|
-
totalCompressed += c;
|
|
36
|
-
calls++;
|
|
37
|
-
const entry = {
|
|
38
|
-
id: Date.now() + Math.random().toString(36).slice(2, 6),
|
|
39
|
-
originalTokens: o,
|
|
40
|
-
compressedTokens: c,
|
|
41
|
-
savedTokens: saved,
|
|
42
|
-
savedPercent: o ? Math.round((1 - c / o) * 100) : 0,
|
|
43
|
-
timestamp: new Date().toISOString(),
|
|
44
|
-
preview: (compressedText || '').slice(0, 120)
|
|
45
|
-
};
|
|
46
|
-
history.unshift(entry);
|
|
47
|
-
if (history.length > MAX_HISTORY) history.pop();
|
|
48
|
-
return { originalTokens: o, compressedTokens: c, savedTokens: saved };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function getStats(budgetLimitOverride, budgetAlertPercentOverride) {
|
|
52
|
-
const limit = (typeof budgetLimitOverride === 'number' && budgetLimitOverride > 0)
|
|
53
|
-
? budgetLimitOverride
|
|
54
|
-
: defaultBudgetLimit;
|
|
55
|
-
const alertPercent = (typeof budgetAlertPercentOverride === 'number' && budgetAlertPercentOverride >= 50 && budgetAlertPercentOverride <= 99)
|
|
56
|
-
? budgetAlertPercentOverride
|
|
57
|
-
: defaultBudgetAlertPercent;
|
|
58
|
-
const saved = Math.max(0, totalOriginal - totalCompressed);
|
|
59
|
-
const pct = totalOriginal ? Math.round((1 - totalCompressed / totalOriginal) * 100) : 0;
|
|
60
|
-
const budgetUsed = totalCompressed;
|
|
61
|
-
const budgetRemaining = Math.max(0, limit - budgetUsed);
|
|
62
|
-
const budgetPercent = Math.min(100, Math.round((budgetUsed / limit) * 100));
|
|
63
|
-
const lowBudget = budgetPercent >= alertPercent;
|
|
64
|
-
return {
|
|
65
|
-
totalOriginal,
|
|
66
|
-
totalCompressed,
|
|
67
|
-
savedTokens: saved,
|
|
68
|
-
savedPercent: pct,
|
|
69
|
-
calls,
|
|
70
|
-
budgetUsed,
|
|
71
|
-
budgetRemaining,
|
|
72
|
-
budgetPercent,
|
|
73
|
-
lowBudget,
|
|
74
|
-
budgetLimit: limit,
|
|
75
|
-
budgetAlertPercent: alertPercent,
|
|
76
|
-
countingStopped: budgetUsed >= limit
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function getHistory() {
|
|
81
|
-
return [...history];
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export function reset() {
|
|
85
|
-
totalOriginal = 0;
|
|
86
|
-
totalCompressed = 0;
|
|
87
|
-
calls = 0;
|
|
88
|
-
history.length = 0;
|
|
89
|
-
}
|