@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
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { estimateTokens } from '../tokens/estimate.js';
|
|
2
1
|
// ponytail: heuristic line filter, not ML — O(n) scan, no deps
|
|
3
2
|
const ANSI_RE = /\u001b\[[0-9;]*[a-zA-Z]/g;
|
|
4
3
|
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;
|
|
5
4
|
const PASS_RE = /^(PASS|\s*✓|\s*✔|\s*ok\s|…+\s*$|\s*\.\s*$|test result:\s*ok|running \d+ test.*ok)/i;
|
|
6
5
|
const NOISE_RE = /^\s*(npm (notice|warn|info)|Browserslist|cached|Downloading|Done in|Compiling\s|cargo:.*Finished)/i;
|
|
7
6
|
const STACK_RE = /^\s*(at\s+|File ".*", line \d+|#\d+\s+0x|goroutine \d+ \[|thread '.*' panicked|note: run with)/;
|
|
7
|
+
const SUMMARY_RE = /(tests?\s+(passed|failed|run)|test result:|suites?\s+\d+|\d+\s+passed|BUILD (SUCCESS|FAILED)|===.*passed.*===|passed in \d+|built in|modules transformed|Finished.*release|Finished.*dev)/i;
|
|
8
8
|
|
|
9
|
-
function cleanAnsi(str) {
|
|
9
|
+
export function cleanAnsi(str) {
|
|
10
10
|
if (!str || !str.includes('\u001b')) return str || '';
|
|
11
11
|
return str.replace(ANSI_RE, '');
|
|
12
12
|
}
|
|
@@ -15,8 +15,14 @@ function keepLine(clean) {
|
|
|
15
15
|
return KEEP_RE.test(clean) || STACK_RE.test(clean);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
function isSummaryLine(clean) {
|
|
19
|
+
return SUMMARY_RE.test(clean);
|
|
20
|
+
}
|
|
21
|
+
|
|
18
22
|
export function compressLog(text, { mode = 'balanced', maxLines = 400 } = {}) {
|
|
19
|
-
if (!text || typeof text !== 'string')
|
|
23
|
+
if (!text || typeof text !== 'string') {
|
|
24
|
+
return { compressed: '', keptLines: 0, totalLines: 0 };
|
|
25
|
+
}
|
|
20
26
|
const lines = text.split(/\r?\n/);
|
|
21
27
|
const totalLines = lines.length;
|
|
22
28
|
if (mode === 'raw') {
|
|
@@ -27,53 +33,70 @@ export function compressLog(text, { mode = 'balanced', maxLines = 400 } = {}) {
|
|
|
27
33
|
if (NOISE_RE.test(clean)) return false;
|
|
28
34
|
return true;
|
|
29
35
|
});
|
|
30
|
-
const truncated = filtered.length > maxLines
|
|
36
|
+
const truncated = filtered.length > maxLines
|
|
37
|
+
? [...filtered.slice(0, maxLines - 1), `… truncated ${filtered.length - maxLines + 1} lines`]
|
|
38
|
+
: filtered;
|
|
31
39
|
const compressed = truncated.join('\n');
|
|
32
|
-
|
|
33
|
-
const compressedTokens = estimateTokens(compressed);
|
|
34
|
-
return { compressed, originalTokens, compressedTokens, savedTokens: Math.max(0, originalTokens - compressedTokens), savedPercent: originalTokens ? Math.round((1 - compressedTokens / originalTokens) * 100) : 0, keptLines: truncated.length, totalLines };
|
|
40
|
+
return { compressed, keptLines: truncated.length, totalLines };
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
const keep = new Array(lines.length).fill(false);
|
|
38
44
|
const cleanLines = new Array(lines.length);
|
|
39
45
|
const context = mode === 'aggressive' ? 1 : 2;
|
|
46
|
+
let hasFailure = false;
|
|
47
|
+
|
|
40
48
|
for (let i = 0; i < lines.length; i++) {
|
|
41
49
|
const clean = cleanAnsi(lines[i]);
|
|
42
50
|
cleanLines[i] = clean;
|
|
43
51
|
if (keepLine(clean)) {
|
|
52
|
+
hasFailure = true;
|
|
44
53
|
const s = Math.max(0, i - context);
|
|
45
54
|
const e = Math.min(lines.length - 1, i + context);
|
|
46
55
|
for (let j = s; j <= e; j++) keep[j] = true;
|
|
47
56
|
}
|
|
48
57
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
58
|
+
|
|
59
|
+
// If no failure was detected (e.g. successful test or build run),
|
|
60
|
+
// collapse into summary: keep first few lines and summary lines
|
|
61
|
+
if (!hasFailure) {
|
|
62
|
+
const head = Math.min(3, lines.length);
|
|
52
63
|
for (let i = 0; i < head; i++) keep[i] = true;
|
|
53
|
-
|
|
54
|
-
|
|
64
|
+
for (let i = head; i < lines.length; i++) {
|
|
65
|
+
if (isSummaryLine(cleanLines[i])) {
|
|
66
|
+
keep[i] = true;
|
|
67
|
+
}
|
|
55
68
|
}
|
|
69
|
+
if (lines.length > 0) keep[lines.length - 1] = true;
|
|
56
70
|
}
|
|
57
|
-
|
|
71
|
+
|
|
72
|
+
// drop pure PASS/noise unless marked
|
|
58
73
|
let out = [];
|
|
59
|
-
for (let i = 0; i < lines.length; i++)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
74
|
+
for (let i = 0; i < lines.length; i++) {
|
|
75
|
+
if (keep[i]) {
|
|
76
|
+
const l = lines[i];
|
|
77
|
+
const clean = cleanLines[i];
|
|
78
|
+
if (mode === 'aggressive' && (PASS_RE.test(clean) || NOISE_RE.test(clean)) && !keepLine(clean) && !isSummaryLine(clean)) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
out.push(l);
|
|
82
|
+
}
|
|
65
83
|
}
|
|
66
84
|
|
|
67
85
|
// collapse consecutive empty lines
|
|
68
86
|
const collapsed = [];
|
|
69
87
|
let emptyStreak = 0;
|
|
70
88
|
for (const l of out) {
|
|
71
|
-
if (l.trim() === '') {
|
|
72
|
-
|
|
89
|
+
if (l.trim() === '') {
|
|
90
|
+
emptyStreak++;
|
|
91
|
+
if (emptyStreak <= 1) collapsed.push(l);
|
|
92
|
+
} else {
|
|
93
|
+
emptyStreak = 0;
|
|
94
|
+
collapsed.push(l);
|
|
95
|
+
}
|
|
73
96
|
}
|
|
74
97
|
out = collapsed;
|
|
75
98
|
|
|
76
|
-
// deduplicate repeated progress dots lines like
|
|
99
|
+
// deduplicate repeated progress dots lines like '................'
|
|
77
100
|
out = out.filter((l, idx, arr) => {
|
|
78
101
|
if (/^[.\s]+$/.test(l) && l.length > 20) return false;
|
|
79
102
|
if (idx > 0 && l === arr[idx - 1] && l.trim() !== '') return false;
|
|
@@ -82,13 +105,15 @@ export function compressLog(text, { mode = 'balanced', maxLines = 400 } = {}) {
|
|
|
82
105
|
|
|
83
106
|
if (out.length > maxLines) {
|
|
84
107
|
const half = Math.floor((maxLines - 1) / 2);
|
|
85
|
-
out = [
|
|
108
|
+
out = [
|
|
109
|
+
...out.slice(0, half),
|
|
110
|
+
`… truncated ${out.length - maxLines + 1} lines …`,
|
|
111
|
+
...out.slice(out.length - half)
|
|
112
|
+
];
|
|
86
113
|
}
|
|
114
|
+
|
|
87
115
|
const compressed = out.join('\n');
|
|
88
|
-
|
|
89
|
-
const compressedTokens = estimateTokens(compressed);
|
|
90
|
-
return { compressed, originalTokens, compressedTokens, savedTokens: Math.max(0, originalTokens - compressedTokens), savedPercent: originalTokens ? Math.round((1 - compressedTokens / originalTokens) * 100) : 0, keptLines: out.length, totalLines };
|
|
116
|
+
return { compressed, keptLines: out.length, totalLines };
|
|
91
117
|
}
|
|
92
118
|
|
|
93
|
-
export {
|
|
94
|
-
export default { compressLog, cleanAnsi, estimateTokens };
|
|
119
|
+
export default { compressLog, cleanAnsi };
|
package/lib/index.js
CHANGED
|
@@ -1,27 +1,13 @@
|
|
|
1
1
|
import z from '@deepseek-ai/schemastery';
|
|
2
|
-
import
|
|
3
|
-
import { compressLog } from './compression/log-compressor.js';
|
|
4
|
-
import { writeJson, readBody, isTrustedRequest } from './http.js';
|
|
5
|
-
import { registerPluginUpdater } from './updater.js';
|
|
6
|
-
import {
|
|
7
|
-
renderOutput,
|
|
8
|
-
registerTools,
|
|
9
|
-
getFocus,
|
|
10
|
-
clearFocus,
|
|
11
|
-
getLastActiveSession
|
|
12
|
-
} from './tools.js';
|
|
2
|
+
import { renderOutput, registerTools } from './tools.js';
|
|
13
3
|
|
|
14
4
|
export const name = '@goodandready/dsh-context-lens';
|
|
15
|
-
export const inject = ['tools', 'settings'
|
|
5
|
+
export const inject = ['tools', 'settings'];
|
|
16
6
|
|
|
17
7
|
export const Config = z.object({
|
|
18
8
|
compressionMode: z.string().default('balanced').description('Log compression aggressiveness (raw/balanced/aggressive)'),
|
|
19
9
|
astSkeletonMaxDepth: z.number().default(3).description('Max depth for AST skeleton generation'),
|
|
20
|
-
|
|
21
|
-
autoCompressThreshold: z.number().default(4000).description('Auto-compress threshold in chars (0 to disable)'),
|
|
22
|
-
budgetLimit: z.number().default(100000).description('Session token budget; compressions stop counting after this limit'),
|
|
23
|
-
budgetAlertPercent: z.number().default(90).description('Budget percentage threshold (50-99%) for warning alert'),
|
|
24
|
-
autoCollapse: z.boolean().default(true).description('Warn in UI when budget is nearly exhausted (does not force-close the settings card)')
|
|
10
|
+
autoCompressThreshold: z.number().default(4000).description('Auto-compress threshold in chars for test/build logs (0 to disable)')
|
|
25
11
|
});
|
|
26
12
|
|
|
27
13
|
const NS = '@goodandready/dsh-context-lens';
|
|
@@ -29,175 +15,74 @@ const NS = '@goodandready/dsh-context-lens';
|
|
|
29
15
|
export { renderOutput };
|
|
30
16
|
|
|
31
17
|
export function apply(ctx, config) {
|
|
32
|
-
let
|
|
18
|
+
let readConfig = () => config;
|
|
19
|
+
const getConfig = () => readConfig();
|
|
33
20
|
|
|
34
21
|
ctx.inject(['settings'], (sctx) => {
|
|
35
|
-
if (
|
|
36
|
-
sctx.effect(() => {
|
|
37
|
-
const scope = sctx.settings.register(NS, Config, { base: config });
|
|
38
|
-
getConfig = () => scope.get() ?? config;
|
|
39
|
-
return () => {
|
|
40
|
-
if (typeof scope?.dispose === 'function') scope.dispose();
|
|
41
|
-
getConfig = () => config;
|
|
42
|
-
};
|
|
43
|
-
}, 'dsh-context-lens: settings');
|
|
44
|
-
} else {
|
|
45
|
-
const scope = sctx.settings.register(NS, Config, { base: config });
|
|
46
|
-
getConfig = () => scope.get() ?? config;
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
function maybeTrack(original, compressed) {
|
|
51
|
-
const cfg = getConfig();
|
|
52
|
-
if (cfg.tokenSavingsTracking === false) return;
|
|
53
|
-
tracker.record(original, compressed);
|
|
54
|
-
}
|
|
22
|
+
if (!sctx?.settings) return;
|
|
55
23
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const mountUpdater = () => registerPluginUpdater(ctx, {
|
|
64
|
-
packageName: '@goodandready/dsh-context-lens',
|
|
65
|
-
manifestUrl: new URL('../package.json', import.meta.url),
|
|
66
|
-
endpoint: '/api/dsh-context-lens/update'
|
|
67
|
-
});
|
|
68
|
-
if (typeof ctx.effect === 'function') {
|
|
69
|
-
ctx.effect(mountUpdater, 'dsh-context-lens: updater');
|
|
70
|
-
} else {
|
|
71
|
-
mountUpdater();
|
|
24
|
+
let disposeConfigure;
|
|
25
|
+
try {
|
|
26
|
+
if (typeof sctx.settings.configure === 'function') {
|
|
27
|
+
disposeConfigure = sctx.settings.configure({ auto: false }, ctx.fiber);
|
|
28
|
+
}
|
|
29
|
+
} catch (error) {
|
|
30
|
+
ctx.logger?.warn?.('dsh-context-lens: settings configure failed: ' + (error instanceof Error ? error.message : String(error)));
|
|
72
31
|
}
|
|
73
32
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
89
|
-
const cfg = getConfig();
|
|
90
|
-
if (typeof cfg.budgetLimit === 'number') tracker.setBudgetLimit(cfg.budgetLimit);
|
|
91
|
-
if (typeof cfg.budgetAlertPercent === 'number') tracker.setBudgetAlertPercent(cfg.budgetAlertPercent);
|
|
92
|
-
let sId = getLastActiveSession() || '__default__';
|
|
93
|
-
try {
|
|
94
|
-
const u = new URL(req.url, 'http://127.0.0.1');
|
|
95
|
-
const qId = u.searchParams.get('sessionId') || u.searchParams.get('session_id');
|
|
96
|
-
if (qId !== null) {
|
|
97
|
-
if (!qId || typeof qId !== 'string' || qId.trim() === '') {
|
|
98
|
-
writeJson(res, 400, { ok: false, error: 'Invalid sessionId parameter' });
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
sId = qId.trim();
|
|
33
|
+
const readLiveSettings = () => {
|
|
34
|
+
try {
|
|
35
|
+
if (typeof sctx.settings.describe === 'function') {
|
|
36
|
+
const desc = sctx.settings.describe()?.find?.((row) => row.ns === NS);
|
|
37
|
+
if (desc && desc.value && typeof desc.value === 'object') {
|
|
38
|
+
return Config(desc.value);
|
|
102
39
|
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
writeJson(res, 200, {
|
|
110
|
-
ok: true,
|
|
111
|
-
plugin: 'dsh-context-lens',
|
|
112
|
-
stats: tracker.getStats(cfg.budgetLimit, cfg.budgetAlertPercent),
|
|
113
|
-
history: tracker.getHistory(),
|
|
114
|
-
focus: getFocus(sId),
|
|
115
|
-
sessionId: sId
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
}), 'dsh-context-lens status route');
|
|
119
|
-
|
|
120
|
-
// Quick clear-focus API route (#62: POST only, trusted origin check; #70: clean URL error handling)
|
|
121
|
-
ctx.effect(() => ctx.webServer.register({
|
|
122
|
-
kind: 'exact',
|
|
123
|
-
path: '/dsh-context-lens/clear-focus',
|
|
124
|
-
handler: async (req, res) => {
|
|
125
|
-
if (req.method !== 'POST') {
|
|
126
|
-
res.writeHead(405, { Allow: 'POST', 'Content-Type': 'application/json' });
|
|
127
|
-
res.end(JSON.stringify({ ok: false, error: 'Method Not Allowed: POST required' }));
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
if (!isTrustedRequest(req)) {
|
|
131
|
-
writeJson(res, 403, { ok: false, error: 'Forbidden: untrusted request origin' });
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
let sId = getLastActiveSession() || '__default__';
|
|
136
|
-
try {
|
|
137
|
-
const u = new URL(req.url, 'http://127.0.0.1');
|
|
138
|
-
const qId = u.searchParams.get('sessionId') || u.searchParams.get('session_id');
|
|
139
|
-
if (qId !== null) {
|
|
140
|
-
if (!qId || typeof qId !== 'string' || qId.trim() === '') {
|
|
141
|
-
writeJson(res, 400, { ok: false, error: 'Invalid sessionId parameter' });
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
sId = qId.trim();
|
|
40
|
+
} else if (typeof sctx.settings.get === 'function') {
|
|
41
|
+
const val = sctx.settings.get(NS);
|
|
42
|
+
if (val && typeof val === 'object') {
|
|
43
|
+
return Config(val);
|
|
145
44
|
}
|
|
146
|
-
} catch (err) {
|
|
147
|
-
ctx.logger?.warn?.('dsh-context-lens: failed to parse request url in clear-focus', err);
|
|
148
|
-
writeJson(res, 400, { ok: false, error: 'Malformed request URL' });
|
|
149
|
-
return;
|
|
150
45
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
writeJson(res, 200, { ok: true, sessionId: sId, focus: getFocus(sId) });
|
|
46
|
+
} catch (error) {
|
|
47
|
+
ctx.logger?.warn?.('dsh-context-lens: settings describe failed: ' + (error instanceof Error ? error.message : String(error)));
|
|
154
48
|
}
|
|
155
|
-
|
|
49
|
+
return null;
|
|
50
|
+
};
|
|
156
51
|
|
|
157
|
-
|
|
158
|
-
ctx.effect(() => ctx.webServer.register({
|
|
159
|
-
kind: 'exact',
|
|
160
|
-
path: '/dsh-context-lens/compress-preview',
|
|
161
|
-
handler: async (req, res) => {
|
|
162
|
-
if (req.method !== 'POST') {
|
|
163
|
-
res.writeHead(405, { Allow: 'POST', 'Content-Type': 'application/json' });
|
|
164
|
-
res.end(JSON.stringify({ ok: false, error: 'Method Not Allowed: POST required' }));
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
if (!isTrustedRequest(req)) {
|
|
168
|
-
writeJson(res, 403, { ok: false, error: 'Forbidden: untrusted request origin' });
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
52
|
+
readConfig = () => readLiveSettings() ?? Config(ctx.fiber?.config ?? config) ?? config;
|
|
171
53
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
54
|
+
if (typeof sctx.effect === 'function') {
|
|
55
|
+
sctx.effect(() => {
|
|
56
|
+
const disposeDocUpdated = typeof sctx.on === 'function'
|
|
57
|
+
? sctx.on('settings/document-updated', (updatedNs) => {
|
|
58
|
+
if (updatedNs === NS) {
|
|
59
|
+
// Live settings refreshed on next readLiveSettings call
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
: null;
|
|
63
|
+
return () => {
|
|
64
|
+
if (typeof disposeConfigure === 'function') {
|
|
65
|
+
try {
|
|
66
|
+
disposeConfigure();
|
|
67
|
+
} catch (err) {
|
|
68
|
+
ctx.logger?.debug?.('dsh-context-lens: disposeConfigure failed', err);
|
|
69
|
+
}
|
|
180
70
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const parsed = Number(body.maxLines);
|
|
188
|
-
if (Number.isNaN(parsed) || parsed < 1 || parsed > 5000) {
|
|
189
|
-
writeJson(res, 400, { ok: false, error: 'maxLines must be a number between 1 and 5000' });
|
|
190
|
-
return;
|
|
71
|
+
if (typeof disposeDocUpdated === 'function') {
|
|
72
|
+
try {
|
|
73
|
+
disposeDocUpdated();
|
|
74
|
+
} catch (err) {
|
|
75
|
+
ctx.logger?.debug?.('dsh-context-lens: disposeDocUpdated failed', err);
|
|
76
|
+
}
|
|
191
77
|
}
|
|
192
|
-
|
|
193
|
-
}
|
|
78
|
+
readConfig = () => config;
|
|
79
|
+
};
|
|
80
|
+
}, 'dsh-context-lens: settings');
|
|
81
|
+
}
|
|
82
|
+
});
|
|
194
83
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const out = compressLog(text, { mode, maxLines });
|
|
199
|
-
writeJson(res, 200, { ok: true, mode, ...out });
|
|
200
|
-
}
|
|
201
|
-
}), 'dsh-context-lens compress-preview route');
|
|
84
|
+
// Register consolidated AI agent tools (context_lens_code, context_lens_log)
|
|
85
|
+
if (ctx.tools) {
|
|
86
|
+
registerTools(ctx, { getConfig });
|
|
202
87
|
}
|
|
203
88
|
}
|