@goodandready/dsh-context-lens 0.1.25 → 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 +9 -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 +5 -168
- 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';
|
|
@@ -95,157 +81,8 @@ export function apply(ctx, config) {
|
|
|
95
81
|
}
|
|
96
82
|
});
|
|
97
83
|
|
|
98
|
-
|
|
99
|
-
const cfg = getConfig();
|
|
100
|
-
if (cfg.tokenSavingsTracking === false) return;
|
|
101
|
-
tracker.record(original, compressed);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Register AI agent tools
|
|
84
|
+
// Register consolidated AI agent tools (context_lens_code, context_lens_log)
|
|
105
85
|
if (ctx.tools) {
|
|
106
|
-
registerTools(ctx, { getConfig
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
if (ctx.webServer) {
|
|
110
|
-
// #61: One-click plugin updater endpoint (/api/dsh-context-lens/update)
|
|
111
|
-
const mountUpdater = () => registerPluginUpdater(ctx, {
|
|
112
|
-
packageName: '@goodandready/dsh-context-lens',
|
|
113
|
-
manifestUrl: new URL('../package.json', import.meta.url),
|
|
114
|
-
endpoint: '/api/dsh-context-lens/update'
|
|
115
|
-
});
|
|
116
|
-
if (typeof ctx.effect === 'function') {
|
|
117
|
-
ctx.effect(mountUpdater, 'dsh-context-lens: updater');
|
|
118
|
-
} else {
|
|
119
|
-
mountUpdater();
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Status route (#70: explicit URL parsing and validation; #87: GET only, trusted source check)
|
|
123
|
-
ctx.effect(() => ctx.webServer.register({
|
|
124
|
-
kind: 'exact',
|
|
125
|
-
path: '/dsh-context-lens/status',
|
|
126
|
-
handler: (req, res) => {
|
|
127
|
-
if (req.method !== 'GET') {
|
|
128
|
-
res.writeHead(405, { Allow: 'GET', 'Content-Type': 'application/json' });
|
|
129
|
-
res.end(JSON.stringify({ ok: false, error: 'Method Not Allowed: GET required' }));
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
if (!isTrustedRequest(req)) {
|
|
133
|
-
writeJson(res, 403, { ok: false, error: 'Forbidden: untrusted request origin' });
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const cfg = getConfig();
|
|
138
|
-
if (typeof cfg.budgetLimit === 'number') tracker.setBudgetLimit(cfg.budgetLimit);
|
|
139
|
-
if (typeof cfg.budgetAlertPercent === 'number') tracker.setBudgetAlertPercent(cfg.budgetAlertPercent);
|
|
140
|
-
let sId = getLastActiveSession() || '__default__';
|
|
141
|
-
try {
|
|
142
|
-
const u = new URL(req.url, 'http://127.0.0.1');
|
|
143
|
-
const qId = u.searchParams.get('sessionId') || u.searchParams.get('session_id');
|
|
144
|
-
if (qId !== null) {
|
|
145
|
-
if (!qId || typeof qId !== 'string' || qId.trim() === '') {
|
|
146
|
-
writeJson(res, 400, { ok: false, error: 'Invalid sessionId parameter' });
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
sId = qId.trim();
|
|
150
|
-
}
|
|
151
|
-
} catch (err) {
|
|
152
|
-
ctx.logger?.warn?.('dsh-context-lens: failed to parse request url in status', err);
|
|
153
|
-
writeJson(res, 400, { ok: false, error: 'Malformed request URL' });
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
writeJson(res, 200, {
|
|
158
|
-
ok: true,
|
|
159
|
-
plugin: 'dsh-context-lens',
|
|
160
|
-
stats: tracker.getStats(cfg.budgetLimit, cfg.budgetAlertPercent),
|
|
161
|
-
history: tracker.getHistory(),
|
|
162
|
-
focus: getFocus(sId),
|
|
163
|
-
sessionId: sId
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
}), 'dsh-context-lens status route');
|
|
167
|
-
|
|
168
|
-
// Quick clear-focus API route (#62: POST only, trusted origin check; #70: clean URL error handling)
|
|
169
|
-
ctx.effect(() => ctx.webServer.register({
|
|
170
|
-
kind: 'exact',
|
|
171
|
-
path: '/dsh-context-lens/clear-focus',
|
|
172
|
-
handler: async (req, res) => {
|
|
173
|
-
if (req.method !== 'POST') {
|
|
174
|
-
res.writeHead(405, { Allow: 'POST', 'Content-Type': 'application/json' });
|
|
175
|
-
res.end(JSON.stringify({ ok: false, error: 'Method Not Allowed: POST required' }));
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
if (!isTrustedRequest(req)) {
|
|
179
|
-
writeJson(res, 403, { ok: false, error: 'Forbidden: untrusted request origin' });
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
let sId = getLastActiveSession() || '__default__';
|
|
184
|
-
try {
|
|
185
|
-
const u = new URL(req.url, 'http://127.0.0.1');
|
|
186
|
-
const qId = u.searchParams.get('sessionId') || u.searchParams.get('session_id');
|
|
187
|
-
if (qId !== null) {
|
|
188
|
-
if (!qId || typeof qId !== 'string' || qId.trim() === '') {
|
|
189
|
-
writeJson(res, 400, { ok: false, error: 'Invalid sessionId parameter' });
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
sId = qId.trim();
|
|
193
|
-
}
|
|
194
|
-
} catch (err) {
|
|
195
|
-
ctx.logger?.warn?.('dsh-context-lens: failed to parse request url in clear-focus', err);
|
|
196
|
-
writeJson(res, 400, { ok: false, error: 'Malformed request URL' });
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
clearFocus(sId);
|
|
201
|
-
writeJson(res, 200, { ok: true, sessionId: sId, focus: getFocus(sId) });
|
|
202
|
-
}
|
|
203
|
-
}), 'dsh-context-lens clear-focus route');
|
|
204
|
-
|
|
205
|
-
// Server-backed preview (#63: POST only, trusted origin, bounded body size, clamped maxLines)
|
|
206
|
-
ctx.effect(() => ctx.webServer.register({
|
|
207
|
-
kind: 'exact',
|
|
208
|
-
path: '/dsh-context-lens/compress-preview',
|
|
209
|
-
handler: async (req, res) => {
|
|
210
|
-
if (req.method !== 'POST') {
|
|
211
|
-
res.writeHead(405, { Allow: 'POST', 'Content-Type': 'application/json' });
|
|
212
|
-
res.end(JSON.stringify({ ok: false, error: 'Method Not Allowed: POST required' }));
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
if (!isTrustedRequest(req)) {
|
|
216
|
-
writeJson(res, 403, { ok: false, error: 'Forbidden: untrusted request origin' });
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
let body;
|
|
221
|
-
try {
|
|
222
|
-
const raw = await readBody(req, 256 * 1024);
|
|
223
|
-
body = JSON.parse(raw.toString('utf8') || '{}');
|
|
224
|
-
} catch (e) {
|
|
225
|
-
if (e?.message === 'body too large') {
|
|
226
|
-
writeJson(res, 413, { ok: false, error: 'Payload Too Large: maximum 256KB allowed' });
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
writeJson(res, 400, { ok: false, error: 'Malformed JSON body' });
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
let maxLines = 400;
|
|
234
|
-
if (body.maxLines !== undefined) {
|
|
235
|
-
const parsed = Number(body.maxLines);
|
|
236
|
-
if (Number.isNaN(parsed) || parsed < 1 || parsed > 5000) {
|
|
237
|
-
writeJson(res, 400, { ok: false, error: 'maxLines must be a number between 1 and 5000' });
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
maxLines = Math.floor(parsed);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const cfg = getConfig();
|
|
244
|
-
const mode = body.mode || cfg.compressionMode || 'balanced';
|
|
245
|
-
const text = typeof body.text === 'string' ? body.text : '';
|
|
246
|
-
const out = compressLog(text, { mode, maxLines });
|
|
247
|
-
writeJson(res, 200, { ok: true, mode, ...out });
|
|
248
|
-
}
|
|
249
|
-
}), 'dsh-context-lens compress-preview route');
|
|
86
|
+
registerTools(ctx, { getConfig });
|
|
250
87
|
}
|
|
251
88
|
}
|
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
|
}
|