@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/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
- }
package/lib/updater.js DELETED
@@ -1,237 +0,0 @@
1
- import { spawn } from 'node:child_process';
2
- import { existsSync, readFileSync } from 'node:fs';
3
- import { readFile } from 'node:fs/promises';
4
- import { homedir } from 'node:os';
5
- import { basename, dirname, isAbsolute, resolve } from 'node:path';
6
- import { fileURLToPath } from 'node:url';
7
- import { writeJson, isTrustedUpdateRequest } from './http.js';
8
-
9
- const VERSION_CACHE_MS = 60_000;
10
- const UPDATE_TIMEOUT_MS = 120_000;
11
-
12
- let latestCache;
13
-
14
- function validProfileName(value) {
15
- return typeof value === 'string' && value !== '' && value !== '.' && value !== '..'
16
- && !value.includes('/') && !value.includes('\\') && !/[\0-\x1f\x7f]/.test(value);
17
- }
18
-
19
- function profileNameFromArgv(argv) {
20
- if (!Array.isArray(argv)) return undefined;
21
- for (let index = 2; index < argv.length; index += 1) {
22
- if (argv[index] === '--profile') return argv[index + 1];
23
- if (typeof argv[index] === 'string' && argv[index].startsWith('--profile=')) {
24
- return argv[index].slice('--profile='.length);
25
- }
26
- }
27
- return undefined;
28
- }
29
-
30
- function findDshCliEntry() {
31
- const value = process.argv[1];
32
- if (typeof value !== 'string' || value.length === 0) return undefined;
33
- const entry = value.startsWith('file:') ? fileURLToPath(value) : resolve(process.cwd(), value);
34
- if (!existsSync(entry)) return undefined;
35
- for (let directory = dirname(entry); ; directory = dirname(directory)) {
36
- const manifestPath = resolve(directory, 'package.json');
37
- if (existsSync(manifestPath)) {
38
- try {
39
- const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
40
- const bin = typeof manifest.bin === 'string'
41
- ? manifest.bin
42
- : typeof manifest.bin === 'object' && manifest.bin !== null
43
- ? manifest.bin.dsh
44
- : undefined;
45
- if (manifest.name === '@deepseek-ai/dsh' && typeof bin === 'string'
46
- && !isAbsolute(bin) && resolve(directory, bin) === resolve(entry)) return entry;
47
- } catch {
48
- /* continue */
49
- }
50
- }
51
- const parent = dirname(directory);
52
- if (parent === directory) return undefined;
53
- }
54
- }
55
-
56
- export function detectRuntime() {
57
- const profileDir = resolve(process.env.DSH_PROFILE_DIR
58
- ?? resolve(homedir(), '.dsh', 'profiles', 'web'));
59
- const selected = profileNameFromArgv(process.argv);
60
- const profileName = validProfileName(selected)
61
- ? selected
62
- : validProfileName(basename(profileDir)) ? basename(profileDir) : 'web';
63
- const cliEntry = findDshCliEntry();
64
- return cliEntry === undefined ? { profileName, profileDir } : { profileName, profileDir, cliEntry };
65
- }
66
-
67
- export function parseSemver(value) {
68
- const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(String(value || ''));
69
- if (match === null) return undefined;
70
- return {
71
- core: [Number(match[1]), Number(match[2]), Number(match[3])],
72
- prerelease: match[4]?.split('.') ?? [],
73
- };
74
- }
75
-
76
- function comparePrerelease(candidateParts, currentParts) {
77
- if (candidateParts.length === 0 && currentParts.length > 0) return 1;
78
- if (candidateParts.length > 0 && currentParts.length === 0) return -1;
79
- if (candidateParts.length === 0 && currentParts.length === 0) return 0;
80
-
81
- const len = Math.max(candidateParts.length, currentParts.length);
82
- for (let i = 0; i < len; i += 1) {
83
- const a = candidateParts[i];
84
- const b = currentParts[i];
85
- if (a === undefined) return -1;
86
- if (b === undefined) return 1;
87
- if (a === b) continue;
88
-
89
- const aNum = /^\d+$/.test(a) ? Number(a) : undefined;
90
- const bNum = /^\d+$/.test(b) ? Number(b) : undefined;
91
-
92
- if (aNum !== undefined && bNum !== undefined) {
93
- return aNum > bNum ? 1 : -1;
94
- }
95
- if (aNum !== undefined && bNum === undefined) {
96
- return -1;
97
- }
98
- if (aNum === undefined && bNum !== undefined) {
99
- return 1;
100
- }
101
- return a.localeCompare(b) > 0 ? 1 : -1;
102
- }
103
- return 0;
104
- }
105
-
106
- export function isNewerVersion(currentValue, candidateValue) {
107
- const current = parseSemver(currentValue);
108
- const candidate = parseSemver(candidateValue);
109
- if (current === undefined || candidate === undefined) return false;
110
- for (let index = 0; index < 3; index += 1) {
111
- if (candidate.core[index] !== current.core[index]) return candidate.core[index] > current.core[index];
112
- }
113
- return comparePrerelease(candidate.prerelease, current.prerelease) > 0;
114
- }
115
-
116
- async function fetchLatestVersion(packageName, registry) {
117
- if (latestCache?.packageName === packageName && latestCache.registry === registry && Date.now() < latestCache.expiresAt) {
118
- return latestCache.version;
119
- }
120
- try {
121
- const reg = registry.replace(/\/+$/, '');
122
- const response = await fetch(`${reg}/${encodeURIComponent(packageName)}/latest`, {
123
- signal: AbortSignal.timeout(6000),
124
- });
125
- if (!response.ok) return undefined;
126
- const value = await response.json();
127
- if (typeof value?.version !== 'string' || !value.version) return undefined;
128
- latestCache = { packageName, registry, version: value.version, expiresAt: Date.now() + VERSION_CACHE_MS };
129
- return value.version;
130
- } catch {
131
- return undefined;
132
- }
133
- }
134
-
135
- async function readCurrentVersion(manifestUrl) {
136
- const value = JSON.parse(await readFile(manifestUrl, 'utf8'));
137
- if (typeof value?.version !== 'string' || !value.version) throw new Error('Cannot read current plugin version.');
138
- return value.version;
139
- }
140
-
141
- export async function checkUpdateStatus(options, target = detectRuntime()) {
142
- const current = await readCurrentVersion(options.manifestUrl);
143
- const latest = await fetchLatestVersion(options.packageName, options.registry || 'https://registry.npmjs.org');
144
- return {
145
- packageName: options.packageName,
146
- currentVersion: current,
147
- ...(latest === undefined ? {} : { latestVersion: latest }),
148
- latestCheckFailed: latest === undefined,
149
- updateAvailable: latest !== undefined && isNewerVersion(current, latest),
150
- profileName: target.profileName,
151
- canAutoUpdate: target.cliEntry !== undefined,
152
- };
153
- }
154
-
155
- async function installExact(target, packageSpec, options) {
156
- if (target.cliEntry === undefined) throw new Error('Automatic update is unavailable in this runtime.');
157
- await new Promise((resolvePromise, reject) => {
158
- // Note: per issue #61 / dsh-clinebot#24 / dsh-agent-loop-guard#38, do NOT disable pnpm minimum release age quarantine
159
- const child = spawn(process.execPath, [
160
- target.cliEntry, 'plugin', '--profile', target.profileName, 'add',
161
- packageSpec,
162
- `--registry=${options.registry || 'https://registry.npmjs.org/'}`,
163
- ], {
164
- cwd: target.profileDir,
165
- windowsHide: true,
166
- stdio: ['ignore', 'pipe', 'pipe'],
167
- env: { ...process.env, NO_COLOR: '1' },
168
- });
169
- let detail = '';
170
- child.stdout?.on('data', (chunk) => { detail = (detail + String(chunk)).slice(-4000); });
171
- child.stderr?.on('data', (chunk) => { detail = (detail + String(chunk)).slice(-4000); });
172
- const timer = setTimeout(() => {
173
- child.kill();
174
- reject(new Error('Update timed out.'));
175
- }, UPDATE_TIMEOUT_MS);
176
- child.once('error', (error) => { clearTimeout(timer); reject(error); });
177
- child.once('exit', (code) => {
178
- clearTimeout(timer);
179
- if (code === 0) resolvePromise();
180
- else reject(new Error(detail.trim() || `Update exited with code ${String(code)}.`));
181
- });
182
- });
183
- }
184
-
185
- export function registerPluginUpdater(ctx, options) {
186
- if (!ctx?.webServer?.register) return () => {};
187
- let installing = false;
188
- return ctx.webServer.register({
189
- kind: 'exact',
190
- path: options.endpoint,
191
- handler: async (request, response) => {
192
- try {
193
- const target = detectRuntime();
194
- if (request.method === 'GET' || request.method === 'HEAD') {
195
- const payload = await checkUpdateStatus(options, target);
196
- writeJson(response, 200, payload);
197
- return;
198
- }
199
- if (request.method !== 'POST') {
200
- response.writeHead(405, { Allow: 'GET, HEAD, POST' });
201
- response.end();
202
- return;
203
- }
204
- if (!isTrustedUpdateRequest(request)) {
205
- writeJson(response, 403, { ok: false, error: 'Rejected non-local or cross-origin update request.' });
206
- return;
207
- }
208
- if (installing) {
209
- writeJson(response, 409, { ok: false, error: 'This plugin is already updating.' });
210
- return;
211
- }
212
- installing = true;
213
- try {
214
- const before = await checkUpdateStatus(options, target);
215
- if (before.latestVersion === undefined) {
216
- writeJson(response, 503, { ok: false, error: 'The latest version is temporarily unavailable.' });
217
- return;
218
- }
219
- if (!before.updateAvailable) {
220
- writeJson(response, 200, before);
221
- return;
222
- }
223
- await installExact(target, `${options.packageName}@${before.latestVersion}`, options);
224
- writeJson(response, 200, {
225
- ...before,
226
- updatedVersion: before.latestVersion,
227
- restartRequired: true,
228
- });
229
- } finally {
230
- installing = false;
231
- }
232
- } catch (error) {
233
- writeJson(response, 500, { ok: false, error: String(error?.message || error) });
234
- }
235
- },
236
- });
237
- }