@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.
@@ -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') return { compressed: '', originalTokens: 0, compressedTokens: 0, savedTokens: 0, savedPercent: 0, keptLines: 0, totalLines: 0 };
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 ? [...filtered.slice(0, maxLines - 1), `… truncated ${filtered.length - maxLines + 1} lines`] : filtered;
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
- const originalTokens = estimateTokens(text);
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
- // if nothing matched, keep first/last few lines as hint
50
- if (!keep.some(Boolean)) {
51
- const head = Math.min(6, lines.length);
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
- if (lines.length > head) {
54
- keep[lines.length - 1] = true;
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
- // drop pure PASS/noise unless within keep window already marked via context
71
+
72
+ // drop pure PASS/noise unless marked
58
73
  let out = [];
59
- for (let i = 0; i < lines.length; i++) if (keep[i]) {
60
- const l = lines[i];
61
- const clean = cleanLines[i];
62
- // aggressive drops more noise even inside window
63
- if (mode === 'aggressive' && (PASS_RE.test(clean) || NOISE_RE.test(clean)) && !keepLine(clean)) continue;
64
- out.push(l);
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() === '') { emptyStreak++; if (emptyStreak <= 1) collapsed.push(l); }
72
- else { emptyStreak = 0; collapsed.push(l); }
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 = [...out.slice(0, half), `… truncated ${out.length - maxLines + 1} lines …`, ...out.slice(out.length - half)];
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
- const originalTokens = estimateTokens(text);
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 { cleanAnsi, estimateTokens };
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 * as tracker from './tokens/tracker.js';
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', 'webServer'];
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
- tokenSavingsTracking: z.boolean().default(true).description('Track and display token budget savings'),
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 getConfig = () => config;
18
+ let readConfig = () => config;
19
+ const getConfig = () => readConfig();
33
20
 
34
21
  ctx.inject(['settings'], (sctx) => {
35
- if (typeof sctx.effect === 'function') {
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
- // Register AI agent tools
57
- if (ctx.tools) {
58
- registerTools(ctx, { getConfig, maybeTrack });
59
- }
60
-
61
- if (ctx.webServer) {
62
- // #61: One-click plugin updater endpoint (/api/dsh-context-lens/update)
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
- // Status route (#70: explicit URL parsing and validation; #87: GET only, trusted source check)
75
- ctx.effect(() => ctx.webServer.register({
76
- kind: 'exact',
77
- path: '/dsh-context-lens/status',
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
-
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
- } catch (err) {
104
- ctx.logger?.warn?.('dsh-context-lens: failed to parse request url in status', err);
105
- writeJson(res, 400, { ok: false, error: 'Malformed request URL' });
106
- return;
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
- clearFocus(sId);
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
- }), 'dsh-context-lens clear-focus route');
49
+ return null;
50
+ };
156
51
 
157
- // Server-backed preview (#63: POST only, trusted origin, bounded body size, clamped maxLines)
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
- let body;
173
- try {
174
- const raw = await readBody(req, 256 * 1024);
175
- body = JSON.parse(raw.toString('utf8') || '{}');
176
- } catch (e) {
177
- if (e?.message === 'body too large') {
178
- writeJson(res, 413, { ok: false, error: 'Payload Too Large: maximum 256KB allowed' });
179
- return;
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
- writeJson(res, 400, { ok: false, error: 'Malformed JSON body' });
182
- return;
183
- }
184
-
185
- let maxLines = 400;
186
- if (body.maxLines !== undefined) {
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
- maxLines = Math.floor(parsed);
193
- }
78
+ readConfig = () => config;
79
+ };
80
+ }, 'dsh-context-lens: settings');
81
+ }
82
+ });
194
83
 
195
- const cfg = getConfig();
196
- const mode = body.mode || cfg.compressionMode || 'balanced';
197
- const text = typeof body.text === 'string' ? body.text : '';
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
  }