@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/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 * as tracker from './tokens/tracker.js';
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 = { type: 'object', properties: { success: { type: 'boolean' } }, additionalProperties: true };
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 getLastActiveSession() {
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: 'context_lens_focus',
79
- description: 'Set active file paths for current session focus (these files skip AST compression)',
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
- paths: { type: 'array', items: { type: 'string' }, description: 'File paths or patterns currently in focus' },
84
- sessionId: { type: 'string', description: 'Session id (uses active session if omitted)' }
85
- },
86
- required: ['paths']
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
- ctx.tools.register({
104
- name: 'context_lens_compress_log',
105
- description: 'Compress a large log/test output block, keeping failures and stacktraces (Jest/Pytest/Go/npm)',
106
- parameters: {
107
- type: 'object',
108
- properties: {
109
- text: { type: 'string', description: 'Raw log text to compress' },
110
- log: { type: 'string', description: 'Alias for text' },
111
- mode: { type: 'string', enum: ['raw', 'balanced', 'aggressive'], description: 'Compression aggressiveness' },
112
- maxLines: { type: 'number', description: 'Max output lines' },
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
- ctx.tools.register({
132
- name: 'context_lens_compress_code',
133
- description: 'Generate AST skeleton for a large source file (JS/TS/Python/Go/Rust/Java/C/C++/SQL) to save context',
134
- parameters: {
135
- type: 'object',
136
- properties: {
137
- code: { type: 'string', description: 'Source code to skeletonize' },
138
- language: { type: 'string', enum: ['js', 'ts', 'py', 'python', 'go', 'rust', 'java', 'c', 'cpp', 'sql'], description: 'Language hint' },
139
- maxDepth: { type: 'number', description: 'Max depth' },
140
- filePath: { type: 'string', description: 'File path to check focus (if in focus, returns full code)' },
141
- sessionId: { type: 'string', description: 'Session id for focus lookup' }
142
- },
143
- required: ['code']
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
- maybeTrack(params.code, params.code);
157
- return { success: true, skeleton: params.code, originalTokens: estLog(params.code), skeletonTokens: estLog(params.code), maxDepth, focused: true, sessionId: key, hint: 'File is in focus, returned full code' };
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
- const skeleton = skeletonize(params.code, { maxDepth, language: params.language });
160
- maybeTrack(params.code, skeleton);
161
- return { success: true, skeleton, originalTokens: estLog(params.code), skeletonTokens: estLog(skeleton), maxDepth, focused: false, sessionId: key };
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: 'context_lens_reset',
167
- description: 'Reset context-lens savings stats and history (e.g. at the start of a new task)',
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
- sessionId: { type: 'string', description: 'Optional session id to also reset session focus' }
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, meta) => {
176
- tracker.reset();
177
- const key = sessionKey(params || {}, meta);
178
- if (params && params.sessionId) {
179
- focusBySession.delete(key);
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
- ctx.tools.register({
186
- name: 'context_lens_stats',
187
- description: 'Show session token savings stats for context-lens',
188
- parameters: {
189
- type: 'object',
190
- properties: {
191
- sessionId: { type: 'string', description: 'Session id for focus snapshot' }
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
- output: { schema: OUTPUT_SCHEMA, render: renderOutput },
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
- ...stats,
205
- history,
206
- trackingEnabled: cfg.tokenSavingsTracking !== false,
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.24",
4
- "description": "DSH plugin for AST context compression, test log filtering, and token budget guard",
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-filter"
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
- }
@@ -1,6 +0,0 @@
1
- // shared token estimate (~chars/4)
2
- export function estimateTokens(text) {
3
- return Math.ceil((text || '').length / 4);
4
- }
5
-
6
- export default { estimateTokens };
@@ -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
- }