@goodandready/dsh-context-lens 0.1.22 → 0.1.24
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 +18 -0
- package/lib/auto-compress.js +18 -2
- package/lib/client.js +4 -4
- package/lib/compression/log-compressor.js +13 -11
- package/lib/index.js +11 -1
- package/lib/tools.js +42 -6
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,24 @@ All notable changes to `@goodandready/dsh-context-lens` will be documented in th
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.1.24] - 2026-09-24
|
|
9
|
+
|
|
10
|
+
### Performance
|
|
11
|
+
- **Optimized log auto-compression**: replaced string array allocation `split('\n').length > 100` with zero-allocation early-exit `hasAtLeastLines(text, 101)` scan, delivering ~55x faster checks on large log outputs (#85).
|
|
12
|
+
- **Fast-path ANSI stripping**: added immediate fast-path return in `cleanAnsi` for strings without `\u001b` and cached cleaned lines during log compression to eliminate duplicate regex execution (#88).
|
|
13
|
+
|
|
14
|
+
### Security
|
|
15
|
+
- **Hardened `/dsh-context-lens/status` route**: enforced HTTP `GET` method (`405 Method Not Allowed` on other methods) and verified trusted source via `isTrustedRequest` (`403 Forbidden` on untrusted origins), closing cross-site telemetry leak (#87).
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- **Prevented session memory leak**: `clearFocus(key)` now explicitly deletes the session key from `focusBySession`, and enforced a maximum capacity limit (`MAX_FOCUS_SESSIONS = 500`) with LRU eviction (#86).
|
|
19
|
+
- **Sanitized focus paths and fixed matching**: filtered out empty and whitespace strings in `context_lens_focus`, and implemented strict path segment matching in `isPathFocused()` to prevent false compression bypass (#89).
|
|
20
|
+
|
|
21
|
+
## 0.1.23
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- Settings no longer wait on the removed settingsScope service. The client uses configForms (#90).
|
|
25
|
+
|
|
8
26
|
## [0.1.22] - 2026-09-19
|
|
9
27
|
|
|
10
28
|
### Fixed
|
package/lib/auto-compress.js
CHANGED
|
@@ -1,7 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fast check whether a text blob contains at least minLines lines
|
|
3
|
+
* without allocating substring arrays in the V8 heap.
|
|
4
|
+
*/
|
|
5
|
+
export function hasAtLeastLines(text, minLines) {
|
|
6
|
+
if (!text || typeof text !== 'string') return false;
|
|
7
|
+
if (minLines <= 1) return text.length > 0;
|
|
8
|
+
let count = 1;
|
|
9
|
+
let pos = -1;
|
|
10
|
+
while ((pos = text.indexOf('\n', pos + 1)) !== -1) {
|
|
11
|
+
count++;
|
|
12
|
+
if (count >= minLines) return true;
|
|
13
|
+
}
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
|
|
1
17
|
/** Decide whether auto-compress should trigger for a log blob. */
|
|
2
18
|
export function shouldAutoCompress(text, threshold) {
|
|
3
19
|
if (!threshold || threshold <= 0) return false;
|
|
4
|
-
return (text || '').length > threshold || (text
|
|
20
|
+
return (text || '').length > threshold || hasAtLeastLines(text, 101);
|
|
5
21
|
}
|
|
6
22
|
|
|
7
|
-
export default { shouldAutoCompress };
|
|
23
|
+
export default { shouldAutoCompress, hasAtLeastLines };
|
package/lib/client.js
CHANGED
|
@@ -620,9 +620,9 @@ window.__ModuleLoader__.load({
|
|
|
620
620
|
const scopeRef = React.useRef(null);
|
|
621
621
|
if (!scopeRef.current && _ctx) {
|
|
622
622
|
try {
|
|
623
|
-
const s = _ctx.
|
|
624
|
-
if (s && typeof s.
|
|
625
|
-
scopeRef.current = s.
|
|
623
|
+
const s = _ctx.configForms || (_ctx.get && _ctx.get('configForms'));
|
|
624
|
+
if (s && typeof s.get === 'function') {
|
|
625
|
+
scopeRef.current = s.get(NS);
|
|
626
626
|
}
|
|
627
627
|
} catch (e) { scopeRef.current = null; }
|
|
628
628
|
}
|
|
@@ -975,7 +975,7 @@ window.__ModuleLoader__.load({
|
|
|
975
975
|
);
|
|
976
976
|
}
|
|
977
977
|
|
|
978
|
-
module.exports.inject = ['slots', 'locale', '
|
|
978
|
+
module.exports.inject = ['slots', 'locale', 'configForms'];
|
|
979
979
|
module.exports.apply = function apply(ctx) {
|
|
980
980
|
let hasEffect = false;
|
|
981
981
|
try {
|
|
@@ -7,13 +7,12 @@ const NOISE_RE = /^\s*(npm (notice|warn|info)|Browserslist|cached|Downloading|Do
|
|
|
7
7
|
const STACK_RE = /^\s*(at\s+|File ".*", line \d+|#\d+\s+0x|goroutine \d+ \[|thread '.*' panicked|note: run with)/;
|
|
8
8
|
|
|
9
9
|
function cleanAnsi(str) {
|
|
10
|
-
|
|
10
|
+
if (!str || !str.includes('\u001b')) return str || '';
|
|
11
|
+
return str.replace(ANSI_RE, '');
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
function keepLine(
|
|
14
|
-
|
|
15
|
-
if (KEEP_RE.test(clean) || STACK_RE.test(clean)) return true;
|
|
16
|
-
return false;
|
|
14
|
+
function keepLine(clean) {
|
|
15
|
+
return KEEP_RE.test(clean) || STACK_RE.test(clean);
|
|
17
16
|
}
|
|
18
17
|
|
|
19
18
|
export function compressLog(text, { mode = 'balanced', maxLines = 400 } = {}) {
|
|
@@ -24,7 +23,7 @@ export function compressLog(text, { mode = 'balanced', maxLines = 400 } = {}) {
|
|
|
24
23
|
// raw: drop only obvious noise and non-failure passes, keep rest
|
|
25
24
|
const filtered = lines.filter((l) => {
|
|
26
25
|
const clean = cleanAnsi(l);
|
|
27
|
-
if (
|
|
26
|
+
if (keepLine(clean)) return true;
|
|
28
27
|
if (NOISE_RE.test(clean)) return false;
|
|
29
28
|
return true;
|
|
30
29
|
});
|
|
@@ -36,9 +35,12 @@ export function compressLog(text, { mode = 'balanced', maxLines = 400 } = {}) {
|
|
|
36
35
|
}
|
|
37
36
|
|
|
38
37
|
const keep = new Array(lines.length).fill(false);
|
|
38
|
+
const cleanLines = new Array(lines.length);
|
|
39
39
|
const context = mode === 'aggressive' ? 1 : 2;
|
|
40
40
|
for (let i = 0; i < lines.length; i++) {
|
|
41
|
-
|
|
41
|
+
const clean = cleanAnsi(lines[i]);
|
|
42
|
+
cleanLines[i] = clean;
|
|
43
|
+
if (keepLine(clean)) {
|
|
42
44
|
const s = Math.max(0, i - context);
|
|
43
45
|
const e = Math.min(lines.length - 1, i + context);
|
|
44
46
|
for (let j = s; j <= e; j++) keep[j] = true;
|
|
@@ -56,9 +58,9 @@ export function compressLog(text, { mode = 'balanced', maxLines = 400 } = {}) {
|
|
|
56
58
|
let out = [];
|
|
57
59
|
for (let i = 0; i < lines.length; i++) if (keep[i]) {
|
|
58
60
|
const l = lines[i];
|
|
59
|
-
const clean =
|
|
61
|
+
const clean = cleanLines[i];
|
|
60
62
|
// aggressive drops more noise even inside window
|
|
61
|
-
if (mode === 'aggressive' && (PASS_RE.test(clean) || NOISE_RE.test(clean)) && !
|
|
63
|
+
if (mode === 'aggressive' && (PASS_RE.test(clean) || NOISE_RE.test(clean)) && !keepLine(clean)) continue;
|
|
62
64
|
out.push(l);
|
|
63
65
|
}
|
|
64
66
|
|
|
@@ -88,5 +90,5 @@ export function compressLog(text, { mode = 'balanced', maxLines = 400 } = {}) {
|
|
|
88
90
|
return { compressed, originalTokens, compressedTokens, savedTokens: Math.max(0, originalTokens - compressedTokens), savedPercent: originalTokens ? Math.round((1 - compressedTokens / originalTokens) * 100) : 0, keptLines: out.length, totalLines };
|
|
89
91
|
}
|
|
90
92
|
|
|
91
|
-
export { estimateTokens };
|
|
92
|
-
export default { compressLog, estimateTokens };
|
|
93
|
+
export { cleanAnsi, estimateTokens };
|
|
94
|
+
export default { compressLog, cleanAnsi, estimateTokens };
|
package/lib/index.js
CHANGED
|
@@ -71,11 +71,21 @@ export function apply(ctx, config) {
|
|
|
71
71
|
mountUpdater();
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
// Status route (#70: explicit URL parsing and validation)
|
|
74
|
+
// Status route (#70: explicit URL parsing and validation; #87: GET only, trusted source check)
|
|
75
75
|
ctx.effect(() => ctx.webServer.register({
|
|
76
76
|
kind: 'exact',
|
|
77
77
|
path: '/dsh-context-lens/status',
|
|
78
78
|
handler: (req, res) => {
|
|
79
|
+
if (req.method !== 'GET') {
|
|
80
|
+
res.writeHead(405, { Allow: 'GET', 'Content-Type': 'application/json' });
|
|
81
|
+
res.end(JSON.stringify({ ok: false, error: 'Method Not Allowed: GET required' }));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (!isTrustedRequest(req)) {
|
|
85
|
+
writeJson(res, 403, { ok: false, error: 'Forbidden: untrusted request origin' });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
79
89
|
const cfg = getConfig();
|
|
80
90
|
if (typeof cfg.budgetLimit === 'number') tracker.setBudgetLimit(cfg.budgetLimit);
|
|
81
91
|
if (typeof cfg.budgetAlertPercent === 'number') tracker.setBudgetAlertPercent(cfg.budgetAlertPercent);
|
package/lib/tools.js
CHANGED
|
@@ -3,6 +3,20 @@ import { skeletonize } from './ast/skeletonizer.js';
|
|
|
3
3
|
import * as tracker from './tokens/tracker.js';
|
|
4
4
|
import { shouldAutoCompress } from './auto-compress.js';
|
|
5
5
|
|
|
6
|
+
function normalizePath(p) {
|
|
7
|
+
return (p || '').replace(/\\/g, '/').replace(/\/+$/, '');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function isPathFocused(pattern, filePath) {
|
|
11
|
+
const normP = normalizePath(pattern);
|
|
12
|
+
const normF = normalizePath(filePath);
|
|
13
|
+
if (!normP || !normF) return false;
|
|
14
|
+
if (normF === normP) return true;
|
|
15
|
+
if (normF.startsWith(normP + '/')) return true;
|
|
16
|
+
if (normF.endsWith('/' + normP)) return true;
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
6
20
|
export const OUTPUT_SCHEMA = { type: 'object', properties: { success: { type: 'boolean' } }, additionalProperties: true };
|
|
7
21
|
|
|
8
22
|
// #71 (GH #2): output.render MUST return ContentBlock[] [{ type: 'text', text: ... }]
|
|
@@ -10,6 +24,7 @@ export const renderOutput = (_args, result) => [
|
|
|
10
24
|
{ type: 'text', text: typeof result === 'string' ? result : JSON.stringify(result, null, 2) }
|
|
11
25
|
];
|
|
12
26
|
|
|
27
|
+
export const MAX_FOCUS_SESSIONS = 500;
|
|
13
28
|
const focusBySession = new Map();
|
|
14
29
|
let lastActiveSession = '__default__';
|
|
15
30
|
|
|
@@ -24,18 +39,34 @@ export function sessionKey(params, meta) {
|
|
|
24
39
|
}
|
|
25
40
|
|
|
26
41
|
export function getFocus(key) {
|
|
27
|
-
|
|
28
|
-
|
|
42
|
+
const k = key || '__default__';
|
|
43
|
+
if (!focusBySession.has(k)) {
|
|
44
|
+
if (focusBySession.size >= MAX_FOCUS_SESSIONS) {
|
|
45
|
+
const oldestKey = focusBySession.keys().next().value;
|
|
46
|
+
if (oldestKey) focusBySession.delete(oldestKey);
|
|
47
|
+
}
|
|
48
|
+
focusBySession.set(k, { paths: [], updatedAt: null });
|
|
49
|
+
}
|
|
50
|
+
return focusBySession.get(k);
|
|
29
51
|
}
|
|
30
52
|
|
|
31
53
|
export function clearFocus(key) {
|
|
32
54
|
if (key) {
|
|
33
|
-
focusBySession.
|
|
55
|
+
focusBySession.delete(key);
|
|
34
56
|
} else {
|
|
35
57
|
focusBySession.clear();
|
|
36
58
|
}
|
|
37
59
|
}
|
|
38
60
|
|
|
61
|
+
export function resetFocusBySession() {
|
|
62
|
+
focusBySession.clear();
|
|
63
|
+
lastActiveSession = '__default__';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function getFocusBySessionSize() {
|
|
67
|
+
return focusBySession.size;
|
|
68
|
+
}
|
|
69
|
+
|
|
39
70
|
export function getLastActiveSession() {
|
|
40
71
|
return lastActiveSession;
|
|
41
72
|
}
|
|
@@ -57,7 +88,11 @@ export function registerTools(ctx, { getConfig, maybeTrack }) {
|
|
|
57
88
|
output: { schema: OUTPUT_SCHEMA, render: renderOutput },
|
|
58
89
|
execute: async (params, meta) => {
|
|
59
90
|
const key = sessionKey(params, meta);
|
|
60
|
-
const
|
|
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);
|
|
61
96
|
const state = getFocus(key);
|
|
62
97
|
state.paths = paths;
|
|
63
98
|
state.updatedAt = new Date().toISOString();
|
|
@@ -113,8 +148,9 @@ export function registerTools(ctx, { getConfig, maybeTrack }) {
|
|
|
113
148
|
const maxDepth = params.maxDepth ?? cfg.astSkeletonMaxDepth ?? 3;
|
|
114
149
|
const key = sessionKey(params, meta);
|
|
115
150
|
const focusState = getFocus(key);
|
|
116
|
-
const
|
|
117
|
-
|
|
151
|
+
const rawFilePath = typeof params.filePath === 'string' ? params.filePath.trim() : '';
|
|
152
|
+
const isFocused = rawFilePath && focusState.paths.length > 0
|
|
153
|
+
? focusState.paths.some(p => isPathFocused(p, rawFilePath))
|
|
118
154
|
: false;
|
|
119
155
|
if (isFocused) {
|
|
120
156
|
maybeTrack(params.code, params.code);
|
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.24",
|
|
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",
|
|
@@ -47,7 +47,8 @@
|
|
|
47
47
|
"platform": "web",
|
|
48
48
|
"inject": [
|
|
49
49
|
"@deepseek-ai/dsh-client-locale",
|
|
50
|
-
"@deepseek-ai/dsh-client-ui-settings"
|
|
50
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
51
|
+
"@deepseek-ai/dsh-client-ui-slots"
|
|
51
52
|
]
|
|
52
53
|
}
|
|
53
54
|
},
|