context-xray 0.1.0
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/LICENSE +21 -0
- package/README.md +127 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +224 -0
- package/dist/client/http.d.ts +37 -0
- package/dist/client/http.js +133 -0
- package/dist/client/index.d.ts +47 -0
- package/dist/client/index.js +79 -0
- package/dist/client/jsonrpc.d.ts +40 -0
- package/dist/client/jsonrpc.js +28 -0
- package/dist/client/stdio.d.ts +55 -0
- package/dist/client/stdio.js +213 -0
- package/dist/client/transport.d.ts +21 -0
- package/dist/client/transport.js +1 -0
- package/dist/discover.d.ts +21 -0
- package/dist/discover.js +126 -0
- package/dist/estimate.d.ts +21 -0
- package/dist/estimate.js +72 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +5 -0
- package/dist/report/terminal.d.ts +2 -0
- package/dist/report/terminal.js +105 -0
- package/dist/types.d.ts +88 -0
- package/dist/types.js +1 -0
- package/dist/weigh.d.ts +5 -0
- package/dist/weigh.js +126 -0
- package/package.json +63 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const useColor = !process.env['NO_COLOR'] && (process.env['FORCE_COLOR'] === '1' || process.stdout.isTTY === true);
|
|
2
|
+
const c = {
|
|
3
|
+
reset: useColor ? '\x1b[0m' : '',
|
|
4
|
+
dim: useColor ? '\x1b[2m' : '',
|
|
5
|
+
bold: useColor ? '\x1b[1m' : '',
|
|
6
|
+
red: useColor ? '\x1b[31m' : '',
|
|
7
|
+
green: useColor ? '\x1b[32m' : '',
|
|
8
|
+
yellow: useColor ? '\x1b[33m' : '',
|
|
9
|
+
cyan: useColor ? '\x1b[36m' : '',
|
|
10
|
+
gray: useColor ? '\x1b[90m' : '',
|
|
11
|
+
};
|
|
12
|
+
const CONTEXT_WINDOW = 200_000;
|
|
13
|
+
const fmt = (n) => n.toLocaleString('en-US');
|
|
14
|
+
/** Visible width, so ANSI codes do not break the column maths. */
|
|
15
|
+
function width(s) {
|
|
16
|
+
return s.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
17
|
+
}
|
|
18
|
+
function pad(s, to) {
|
|
19
|
+
const w = width(s);
|
|
20
|
+
return w >= to ? s : s + ' '.repeat(to - w);
|
|
21
|
+
}
|
|
22
|
+
function padLeft(s, to) {
|
|
23
|
+
const w = width(s);
|
|
24
|
+
return w >= to ? s : ' '.repeat(to - w) + s;
|
|
25
|
+
}
|
|
26
|
+
function bar(fraction, widthChars = 18) {
|
|
27
|
+
const filled = Math.round(Math.max(0, Math.min(1, fraction)) * widthChars);
|
|
28
|
+
return '#'.repeat(filled) + '.'.repeat(widthChars - filled);
|
|
29
|
+
}
|
|
30
|
+
/** "Claude Desktop, Cursor" -- host names only, config paths live in detail. */
|
|
31
|
+
function hostsOf(w) {
|
|
32
|
+
const hosts = new Set(w.spec.sources.map((s) => s.split(' (')[0] ?? s));
|
|
33
|
+
return [...hosts].join(', ');
|
|
34
|
+
}
|
|
35
|
+
function tokensLabel(w) {
|
|
36
|
+
return `${w.method === 'counted' ? '' : '~'}${fmt(w.taxTokens)}`;
|
|
37
|
+
}
|
|
38
|
+
export function renderTerminal(report, top = 3) {
|
|
39
|
+
const lines = [];
|
|
40
|
+
const measured = report.servers.filter((s) => s.ok);
|
|
41
|
+
const broken = report.servers.filter((s) => !s.ok);
|
|
42
|
+
const ranked = [...measured].sort((a, b) => b.taxTokens - a.taxTokens);
|
|
43
|
+
const total = report.totalTaxTokens;
|
|
44
|
+
const approx = report.method === 'counted' ? '' : '~';
|
|
45
|
+
lines.push('');
|
|
46
|
+
lines.push(` ${c.bold}context-xray${c.reset} your MCP servers add ${c.bold}${approx}${fmt(total)} tokens${c.reset} to every request`);
|
|
47
|
+
const cfgs = report.configsSearched.length;
|
|
48
|
+
lines.push(` ${c.gray}${cfgs} config${cfgs === 1 ? '' : 's'} searched, ${report.servers.length} server${report.servers.length === 1 ? '' : 's'} found, ${measured.length} measured${broken.length ? `, ${broken.length} failed` : ''}${c.reset}`);
|
|
49
|
+
lines.push('');
|
|
50
|
+
if (ranked.length > 0) {
|
|
51
|
+
lines.push(` ${c.gray}${pad('rank', 6)}${pad('server', 22)}${pad('host', 18)}${padLeft('tools', 6)}${padLeft('tokens', 10)}${padLeft('share', 7)}${c.reset}`);
|
|
52
|
+
ranked.forEach((w, i) => {
|
|
53
|
+
const share = total > 0 ? w.taxTokens / total : 0;
|
|
54
|
+
const name = w.spec.name.length > 20 ? w.spec.name.slice(0, 19) + '…' : w.spec.name;
|
|
55
|
+
lines.push(` ${pad(`#${i + 1}`, 6)}${pad(`${c.bold}${name}${c.reset}`, 22 + (c.bold.length + c.reset.length))}${pad(`${c.gray}${hostsOf(w).slice(0, 16)}${c.reset}`, 18 + (c.gray.length + c.reset.length))}${padLeft(String(w.toolCount), 6)}${padLeft(tokensLabel(w), 10)}${padLeft(`${Math.round(share * 100)}%`, 7)} ${c.cyan}${bar(share)}${c.reset}`);
|
|
56
|
+
});
|
|
57
|
+
lines.push('');
|
|
58
|
+
}
|
|
59
|
+
// Per-server detail: the heaviest tools are where the trimming happens.
|
|
60
|
+
for (const w of ranked) {
|
|
61
|
+
if (w.tools.length === 0)
|
|
62
|
+
continue;
|
|
63
|
+
const info = w.serverInfo?.name ? `${w.serverInfo.name}${w.serverInfo.version ? ` v${w.serverInfo.version}` : ''}` : '';
|
|
64
|
+
lines.push(` ${c.bold}${w.spec.name}${c.reset} ${c.gray}${info}${info ? ' ' : ''}${w.toolCount} tools${w.instructionTokens ? `, instructions ${approx}${fmt(w.instructionTokens)} tok` : ''}${w.connectMs !== undefined ? `, connected in ${w.connectMs}ms` : ''}${c.reset}`);
|
|
65
|
+
for (const t of w.tools.slice(0, top)) {
|
|
66
|
+
lines.push(` ${padLeft(fmt(t.tokens), 8)} tok ${pad(t.name.slice(0, 40), 42)}${c.gray}desc ${fmt(t.descriptionTokens)}, schema ${fmt(t.schemaTokens)}${c.reset}`);
|
|
67
|
+
}
|
|
68
|
+
if (w.tools.length > top) {
|
|
69
|
+
const rest = w.tools.slice(top).reduce((s, t) => s + t.tokens, 0);
|
|
70
|
+
lines.push(` ${padLeft(fmt(rest), 8)} tok ${c.gray}… ${w.tools.length - top} more tools${c.reset}`);
|
|
71
|
+
}
|
|
72
|
+
lines.push('');
|
|
73
|
+
}
|
|
74
|
+
for (const w of broken) {
|
|
75
|
+
lines.push(` ${c.red}failed${c.reset} ${c.bold}${w.spec.name}${c.reset} ${c.gray}${w.error}${c.reset}`);
|
|
76
|
+
}
|
|
77
|
+
if (broken.length)
|
|
78
|
+
lines.push('');
|
|
79
|
+
// The verdict block: window share and money.
|
|
80
|
+
const windowShare = total / CONTEXT_WINDOW;
|
|
81
|
+
const monthlyTokens = total * report.options.requestsPerDay * 30;
|
|
82
|
+
const monthlyUsd = (monthlyTokens / 1_000_000) * report.options.pricePerMTok;
|
|
83
|
+
lines.push(` ${c.gray}${'-'.repeat(72)}${c.reset}`);
|
|
84
|
+
lines.push(` ${c.bold}${approx}${fmt(total)} tokens${c.reset} on every request = ${c.bold}${(windowShare * 100).toFixed(1)}%${c.reset} of a ${fmt(CONTEXT_WINDOW)} context window, before you type a word`);
|
|
85
|
+
lines.push(` ${c.gray}at ${fmt(report.options.requestsPerDay)} requests/day and $${report.options.pricePerMTok.toFixed(2)}/MTok input: ${c.reset}${c.bold}${approx}$${monthlyUsd.toFixed(monthlyUsd >= 100 ? 0 : 2)}/month${c.reset}${c.gray} of uncached input spend${c.reset}`);
|
|
86
|
+
if (report.method !== 'counted') {
|
|
87
|
+
lines.push(` ${c.gray}estimated without a tokenizer; set ANTHROPIC_API_KEY and pass --precise for exact counts${c.reset}`);
|
|
88
|
+
}
|
|
89
|
+
// Advice only when there is something to act on.
|
|
90
|
+
const advice = [];
|
|
91
|
+
if (windowShare > 0.1 && ranked.length > 1) {
|
|
92
|
+
advice.push(`over 10% of the window goes to tool definitions -- disable the servers you are not using today (${ranked[0].spec.name} alone is ${Math.round(((ranked[0].taxTokens) / total) * 100)}%)`);
|
|
93
|
+
}
|
|
94
|
+
const fatTool = ranked.flatMap((w) => w.tools.map((t) => ({ server: w.spec.name, t }))).find((x) => x.t.tokens > 600);
|
|
95
|
+
if (fatTool) {
|
|
96
|
+
advice.push(`${fatTool.server}'s "${fatTool.t.name}" costs ${fmt(fatTool.t.tokens)} tokens by itself -- a tool description is a prompt you pay for on every request`);
|
|
97
|
+
}
|
|
98
|
+
if (advice.length > 0) {
|
|
99
|
+
lines.push('');
|
|
100
|
+
for (const a of advice)
|
|
101
|
+
lines.push(` ${c.yellow}!${c.reset} ${a}`);
|
|
102
|
+
}
|
|
103
|
+
lines.push('');
|
|
104
|
+
return lines.join('\n');
|
|
105
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export interface ToolDef {
|
|
2
|
+
name: string;
|
|
3
|
+
description?: string;
|
|
4
|
+
inputSchema?: JsonSchema;
|
|
5
|
+
outputSchema?: JsonSchema;
|
|
6
|
+
annotations?: Record<string, unknown>;
|
|
7
|
+
title?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface JsonSchema {
|
|
10
|
+
type?: string | string[];
|
|
11
|
+
properties?: Record<string, JsonSchema>;
|
|
12
|
+
required?: string[];
|
|
13
|
+
items?: JsonSchema | JsonSchema[];
|
|
14
|
+
enum?: unknown[];
|
|
15
|
+
const?: unknown;
|
|
16
|
+
description?: string;
|
|
17
|
+
additionalProperties?: boolean | JsonSchema;
|
|
18
|
+
[k: string]: unknown;
|
|
19
|
+
}
|
|
20
|
+
/** One MCP server as found in a host config file (or given on the CLI). */
|
|
21
|
+
export interface ServerSpec {
|
|
22
|
+
name: string;
|
|
23
|
+
kind: 'stdio' | 'http';
|
|
24
|
+
command?: string;
|
|
25
|
+
args?: string[];
|
|
26
|
+
env?: Record<string, string>;
|
|
27
|
+
cwd?: string;
|
|
28
|
+
url?: string;
|
|
29
|
+
headers?: Record<string, string>;
|
|
30
|
+
/** Which config files declared this server (a server can appear in several). */
|
|
31
|
+
sources: string[];
|
|
32
|
+
/** Set when the entry cannot be launched (e.g. it needs interactive input). */
|
|
33
|
+
unlaunchable?: string;
|
|
34
|
+
}
|
|
35
|
+
/** The token weight of a single tool definition, as the host serialises it. */
|
|
36
|
+
export interface ToolWeight {
|
|
37
|
+
name: string;
|
|
38
|
+
/** Whole definition: name + description + inputSchema. */
|
|
39
|
+
tokens: number;
|
|
40
|
+
descriptionTokens: number;
|
|
41
|
+
schemaTokens: number;
|
|
42
|
+
}
|
|
43
|
+
export interface ServerWeight {
|
|
44
|
+
spec: ServerSpec;
|
|
45
|
+
ok: boolean;
|
|
46
|
+
/** Why the measurement failed, when it did. */
|
|
47
|
+
error?: string;
|
|
48
|
+
connectMs?: number;
|
|
49
|
+
serverInfo?: {
|
|
50
|
+
name?: string;
|
|
51
|
+
version?: string;
|
|
52
|
+
} | null;
|
|
53
|
+
protocolVersion?: string | null;
|
|
54
|
+
tools: ToolWeight[];
|
|
55
|
+
toolCount: number;
|
|
56
|
+
resourceCount: number;
|
|
57
|
+
promptCount: number;
|
|
58
|
+
/** Sum of all tool-definition tokens: the per-request tax this server charges. */
|
|
59
|
+
toolTokens: number;
|
|
60
|
+
/** Tokens in the server's `instructions` string, which hosts also inject. */
|
|
61
|
+
instructionTokens: number;
|
|
62
|
+
/** toolTokens + instructionTokens. */
|
|
63
|
+
taxTokens: number;
|
|
64
|
+
/** 'estimate' (chars-based) or 'counted' (Anthropic count_tokens). */
|
|
65
|
+
method: 'estimate' | 'counted';
|
|
66
|
+
}
|
|
67
|
+
export interface XrayOptions {
|
|
68
|
+
timeoutMs: number;
|
|
69
|
+
/** Use the Anthropic count_tokens API when a key is available. */
|
|
70
|
+
precise: boolean;
|
|
71
|
+
apiKey?: string;
|
|
72
|
+
/** Assumed requests per day for the cost projection. */
|
|
73
|
+
requestsPerDay: number;
|
|
74
|
+
/** $ per million input tokens for the cost projection. */
|
|
75
|
+
pricePerMTok: number;
|
|
76
|
+
/** How many tools to show per server in the report. */
|
|
77
|
+
top: number;
|
|
78
|
+
concurrency: number;
|
|
79
|
+
}
|
|
80
|
+
export interface XrayReport {
|
|
81
|
+
servers: ServerWeight[];
|
|
82
|
+
/** Sum of taxTokens across servers that measured OK. */
|
|
83
|
+
totalTaxTokens: number;
|
|
84
|
+
method: 'estimate' | 'counted' | 'mixed';
|
|
85
|
+
options: Pick<XrayOptions, 'requestsPerDay' | 'pricePerMTok'>;
|
|
86
|
+
configsSearched: string[];
|
|
87
|
+
durationMs: number;
|
|
88
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/weigh.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ServerSpec, ServerWeight, XrayOptions } from './types.js';
|
|
2
|
+
/** Connect to one server and measure what it adds to every request. */
|
|
3
|
+
export declare function weighServer(spec: ServerSpec, options: XrayOptions): Promise<ServerWeight>;
|
|
4
|
+
/** Weigh every server with bounded concurrency, preserving input order. */
|
|
5
|
+
export declare function weighAll(specs: ServerSpec[], options: XrayOptions): Promise<ServerWeight[]>;
|
package/dist/weigh.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { McpClient, StdioTransport, HttpTransport } from './client/index.js';
|
|
2
|
+
import { estimateTokens, countToolTokens, wireFormat } from './estimate.js';
|
|
3
|
+
function buildTransport(spec) {
|
|
4
|
+
if (spec.kind === 'http') {
|
|
5
|
+
return new HttpTransport({ url: spec.url, headers: spec.headers });
|
|
6
|
+
}
|
|
7
|
+
return new StdioTransport({
|
|
8
|
+
command: spec.command,
|
|
9
|
+
args: spec.args ?? [],
|
|
10
|
+
env: spec.env,
|
|
11
|
+
cwd: spec.cwd,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
function weighTool(tool) {
|
|
15
|
+
const whole = JSON.stringify(wireFormat(tool));
|
|
16
|
+
const description = tool.description ?? '';
|
|
17
|
+
const schema = JSON.stringify(tool.inputSchema ?? {});
|
|
18
|
+
return {
|
|
19
|
+
name: tool.name ?? '(unnamed)',
|
|
20
|
+
tokens: estimateTokens(whole),
|
|
21
|
+
descriptionTokens: estimateTokens(description),
|
|
22
|
+
schemaTokens: estimateTokens(schema),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const failed = (spec, error) => ({
|
|
26
|
+
spec,
|
|
27
|
+
ok: false,
|
|
28
|
+
error,
|
|
29
|
+
tools: [],
|
|
30
|
+
toolCount: 0,
|
|
31
|
+
resourceCount: 0,
|
|
32
|
+
promptCount: 0,
|
|
33
|
+
toolTokens: 0,
|
|
34
|
+
instructionTokens: 0,
|
|
35
|
+
taxTokens: 0,
|
|
36
|
+
method: 'estimate',
|
|
37
|
+
});
|
|
38
|
+
/** Connect to one server and measure what it adds to every request. */
|
|
39
|
+
export async function weighServer(spec, options) {
|
|
40
|
+
if (spec.unlaunchable)
|
|
41
|
+
return failed(spec, `not launched: ${spec.unlaunchable}`);
|
|
42
|
+
const transport = buildTransport(spec);
|
|
43
|
+
const client = new McpClient(transport, options.timeoutMs);
|
|
44
|
+
try {
|
|
45
|
+
await client.start();
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
return failed(spec, e.message);
|
|
49
|
+
}
|
|
50
|
+
let handshake;
|
|
51
|
+
const t0 = Date.now();
|
|
52
|
+
try {
|
|
53
|
+
handshake = await client.initialize();
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
await client.close();
|
|
57
|
+
const stderrTail = transport.stderr.slice(-3).join(' | ');
|
|
58
|
+
return failed(spec, `${e.message}${stderrTail ? ` -- stderr: ${stderrTail}` : ''}`);
|
|
59
|
+
}
|
|
60
|
+
const connectMs = Date.now() - t0;
|
|
61
|
+
if (handshake.raw.error) {
|
|
62
|
+
await client.close();
|
|
63
|
+
return failed(spec, `initialize failed: ${handshake.raw.error.code} ${handshake.raw.error.message}`);
|
|
64
|
+
}
|
|
65
|
+
client.notifyInitialized();
|
|
66
|
+
const { tools } = await client.listTools().catch(() => ({ tools: [] }));
|
|
67
|
+
const resources = await client.listAll('resources/list', 'resources').then((r) => r.items).catch(() => []);
|
|
68
|
+
const prompts = await client.listAll('prompts/list', 'prompts').then((r) => r.items).catch(() => []);
|
|
69
|
+
const instructions = typeof handshake.raw.result?.['instructions'] === 'string'
|
|
70
|
+
? handshake.raw.result['instructions']
|
|
71
|
+
: '';
|
|
72
|
+
const toolWeights = tools.map(weighTool).sort((a, b) => b.tokens - a.tokens);
|
|
73
|
+
let toolTokens = toolWeights.reduce((sum, t) => sum + t.tokens, 0);
|
|
74
|
+
let method = 'estimate';
|
|
75
|
+
// Ground truth when asked for and possible. The per-tool split stays an
|
|
76
|
+
// estimate (the API prices the whole array), but the headline number -- the
|
|
77
|
+
// per-request tax -- becomes exact, scaled through the individual tools so
|
|
78
|
+
// the split still adds up.
|
|
79
|
+
if (options.precise && options.apiKey && tools.length > 0) {
|
|
80
|
+
try {
|
|
81
|
+
const counted = await countToolTokens(options.apiKey, tools);
|
|
82
|
+
if (counted > 0 && toolTokens > 0) {
|
|
83
|
+
const scale = counted / toolTokens;
|
|
84
|
+
for (const t of toolWeights)
|
|
85
|
+
t.tokens = Math.round(t.tokens * scale);
|
|
86
|
+
toolTokens = counted;
|
|
87
|
+
method = 'counted';
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// The estimate still stands; precision is an upgrade, not a requirement.
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const instructionTokens = estimateTokens(instructions);
|
|
95
|
+
await client.close();
|
|
96
|
+
return {
|
|
97
|
+
spec,
|
|
98
|
+
ok: true,
|
|
99
|
+
connectMs,
|
|
100
|
+
serverInfo: handshake.serverInfo,
|
|
101
|
+
protocolVersion: handshake.protocolVersion,
|
|
102
|
+
tools: toolWeights,
|
|
103
|
+
toolCount: tools.length,
|
|
104
|
+
resourceCount: resources.length,
|
|
105
|
+
promptCount: prompts.length,
|
|
106
|
+
toolTokens,
|
|
107
|
+
instructionTokens,
|
|
108
|
+
taxTokens: toolTokens + instructionTokens,
|
|
109
|
+
method,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/** Weigh every server with bounded concurrency, preserving input order. */
|
|
113
|
+
export async function weighAll(specs, options) {
|
|
114
|
+
const results = new Array(specs.length);
|
|
115
|
+
let next = 0;
|
|
116
|
+
const workers = Array.from({ length: Math.max(1, Math.min(options.concurrency, specs.length)) }, async () => {
|
|
117
|
+
for (;;) {
|
|
118
|
+
const i = next++;
|
|
119
|
+
if (i >= specs.length)
|
|
120
|
+
return;
|
|
121
|
+
results[i] = await weighServer(specs[i], options);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
await Promise.all(workers);
|
|
125
|
+
return results;
|
|
126
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "context-xray",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "See what your MCP servers cost you. Measures the context-window tokens every configured MCP server injects into each request - before you type a word - across Claude Desktop, Claude Code, Cursor and VS Code. Zero dependencies.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"model-context-protocol",
|
|
8
|
+
"mcp-server",
|
|
9
|
+
"context-window",
|
|
10
|
+
"token-usage",
|
|
11
|
+
"token-cost",
|
|
12
|
+
"tokens",
|
|
13
|
+
"context",
|
|
14
|
+
"claude",
|
|
15
|
+
"claude-code",
|
|
16
|
+
"cursor",
|
|
17
|
+
"llm",
|
|
18
|
+
"ai-agents",
|
|
19
|
+
"cost",
|
|
20
|
+
"audit",
|
|
21
|
+
"cli",
|
|
22
|
+
"anthropic"
|
|
23
|
+
],
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"type": "module",
|
|
26
|
+
"bin": {
|
|
27
|
+
"context-xray": "dist/cli.js"
|
|
28
|
+
},
|
|
29
|
+
"main": "dist/index.js",
|
|
30
|
+
"types": "dist/index.d.ts",
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"import": "./dist/index.js"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"dist",
|
|
39
|
+
"README.md",
|
|
40
|
+
"LICENSE"
|
|
41
|
+
],
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=18"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsc -p tsconfig.json",
|
|
47
|
+
"test": "npm run build && node --test",
|
|
48
|
+
"prepublishOnly": "npm run build"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/node": "^22.10.0",
|
|
52
|
+
"typescript": "^5.7.0"
|
|
53
|
+
},
|
|
54
|
+
"repository": {
|
|
55
|
+
"type": "git",
|
|
56
|
+
"url": "git+https://github.com/Beeeeen/context-xray.git"
|
|
57
|
+
},
|
|
58
|
+
"homepage": "https://github.com/Beeeeen/context-xray#readme",
|
|
59
|
+
"bugs": {
|
|
60
|
+
"url": "https://github.com/Beeeeen/context-xray/issues"
|
|
61
|
+
},
|
|
62
|
+
"author": "Ben Yang <ben@yangjiawei.com>"
|
|
63
|
+
}
|