@safetnsr/vet 1.20.0 → 1.21.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.
@@ -0,0 +1,3 @@
1
+ import type { CheckResult } from '../types.js';
2
+ export declare function checkContext(cwd: string): CheckResult;
3
+ export declare function runContextCommand(format: string, cwd: string): Promise<void>;
@@ -0,0 +1,359 @@
1
+ import { join } from 'node:path';
2
+ import { existsSync, readdirSync, statSync, readFileSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { c } from '../util.js';
5
+ import { cachedReadFile } from '../file-cache.js';
6
+ // ── Tiktoken lazy init ───────────────────────────────────────────────────────
7
+ import { encodingForModel } from 'js-tiktoken';
8
+ let _encoder = null;
9
+ function getEncoder() {
10
+ if (!_encoder) {
11
+ _encoder = encodingForModel('gpt-4');
12
+ }
13
+ return _encoder;
14
+ }
15
+ function countTokens(text) {
16
+ return getEncoder().encode(text).length;
17
+ }
18
+ // ── Constants ────────────────────────────────────────────────────────────────
19
+ const CONTEXT_FILES = ['CLAUDE.md', 'AGENTS.md', 'SOUL.md', '.cursorrules', 'codex.md'];
20
+ const CURSOR_RULES_DIR = join('.cursor', 'rules');
21
+ const MEMORY_DIR = 'memory';
22
+ const DAILY_DIR = join(MEMORY_DIR, 'daily');
23
+ const MODEL_COSTS = {
24
+ opus: 15, // $15 per MTok input
25
+ sonnet: 3, // $3 per MTok input
26
+ haiku: 0.25, // $0.25 per MTok input
27
+ };
28
+ const TOKEN_THRESHOLD = 8000;
29
+ const BLOATED_FILE_THRESHOLD = 10000;
30
+ function splitIntoSections(content, file) {
31
+ const lines = content.split('\n');
32
+ const sections = [];
33
+ let currentTitle = '(intro)';
34
+ let currentLines = [];
35
+ for (const line of lines) {
36
+ const headerMatch = line.match(/^(#{2,3})\s+(.+)/);
37
+ if (headerMatch) {
38
+ // flush previous
39
+ if (currentLines.length > 0) {
40
+ const text = currentLines.join('\n');
41
+ sections.push({ title: currentTitle, content: text, tokens: countTokens(text), file });
42
+ }
43
+ currentTitle = headerMatch[2].trim();
44
+ currentLines = [line];
45
+ }
46
+ else {
47
+ currentLines.push(line);
48
+ }
49
+ }
50
+ // flush last
51
+ if (currentLines.length > 0) {
52
+ const text = currentLines.join('\n');
53
+ sections.push({ title: currentTitle, content: text, tokens: countTokens(text), file });
54
+ }
55
+ return sections;
56
+ }
57
+ // ── File discovery ───────────────────────────────────────────────────────────
58
+ function discoverContextFiles(cwd) {
59
+ const files = [];
60
+ for (const name of CONTEXT_FILES) {
61
+ const full = join(cwd, name);
62
+ if (existsSync(full))
63
+ files.push(full);
64
+ }
65
+ // memory/*.md (not daily/)
66
+ const memDir = join(cwd, MEMORY_DIR);
67
+ if (existsSync(memDir) && statSync(memDir).isDirectory()) {
68
+ try {
69
+ for (const entry of readdirSync(memDir)) {
70
+ if (!entry.endsWith('.md'))
71
+ continue;
72
+ const full = join(memDir, entry);
73
+ try {
74
+ if (statSync(full).isFile())
75
+ files.push(full);
76
+ }
77
+ catch { /* skip */ }
78
+ }
79
+ }
80
+ catch { /* skip */ }
81
+ }
82
+ // .cursor/rules
83
+ const cursorDir = join(cwd, CURSOR_RULES_DIR);
84
+ if (existsSync(cursorDir) && statSync(cursorDir).isDirectory()) {
85
+ try {
86
+ for (const entry of readdirSync(cursorDir)) {
87
+ const full = join(cursorDir, entry);
88
+ try {
89
+ if (statSync(full).isFile())
90
+ files.push(full);
91
+ }
92
+ catch { /* skip */ }
93
+ }
94
+ }
95
+ catch { /* skip */ }
96
+ }
97
+ return files;
98
+ }
99
+ // ── Stale detection ──────────────────────────────────────────────────────────
100
+ function detectStaleSections(sections) {
101
+ const stale = new Set();
102
+ const claudeDir = join(homedir(), '.claude', 'projects');
103
+ if (!existsSync(claudeDir))
104
+ return stale;
105
+ // Collect recent session log content
106
+ const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
107
+ let sessionContent = '';
108
+ try {
109
+ const projects = readdirSync(claudeDir);
110
+ for (const project of projects) {
111
+ const projectDir = join(claudeDir, project);
112
+ try {
113
+ if (!statSync(projectDir).isDirectory())
114
+ continue;
115
+ }
116
+ catch {
117
+ continue;
118
+ }
119
+ try {
120
+ const files = readdirSync(projectDir);
121
+ for (const file of files) {
122
+ if (!file.endsWith('.jsonl'))
123
+ continue;
124
+ const full = join(projectDir, file);
125
+ try {
126
+ const stat = statSync(full);
127
+ if (stat.mtimeMs < sevenDaysAgo)
128
+ continue;
129
+ sessionContent += readFileSync(full, 'utf-8') + '\n';
130
+ }
131
+ catch { /* skip */ }
132
+ }
133
+ }
134
+ catch { /* skip */ }
135
+ }
136
+ }
137
+ catch {
138
+ return stale;
139
+ }
140
+ if (!sessionContent)
141
+ return stale;
142
+ // Check each section: extract significant phrases and grep
143
+ for (const section of sections) {
144
+ if (section.title === '(intro)')
145
+ continue;
146
+ // Extract significant words from section content (skip short/generic)
147
+ const words = section.content
148
+ .replace(/[#`*_\-\[\](){}|]/g, ' ')
149
+ .split(/\s+/)
150
+ .filter(w => w.length > 4);
151
+ // Take a sample of phrases to check
152
+ const phrases = words.slice(0, 20);
153
+ if (phrases.length === 0)
154
+ continue;
155
+ const found = phrases.some(phrase => sessionContent.includes(phrase));
156
+ if (!found) {
157
+ stale.add(`${section.file}::${section.title}`);
158
+ }
159
+ }
160
+ return stale;
161
+ }
162
+ // ── Cost calculation ─────────────────────────────────────────────────────────
163
+ function calculateCost(tokens, model) {
164
+ const rate = MODEL_COSTS[model] || 3;
165
+ return (tokens / 1_000_000) * rate;
166
+ }
167
+ function formatCost(cost) {
168
+ if (cost < 0.001)
169
+ return `$${cost.toFixed(6)}`;
170
+ if (cost < 0.01)
171
+ return `$${cost.toFixed(4)}`;
172
+ return `$${cost.toFixed(3)}`;
173
+ }
174
+ // ── CheckResult (for full vet scan) ──────────────────────────────────────────
175
+ export function checkContext(cwd) {
176
+ const files = discoverContextFiles(cwd);
177
+ const issues = [];
178
+ let score = 100;
179
+ if (files.length === 0) {
180
+ score -= 15;
181
+ issues.push({
182
+ severity: 'error',
183
+ message: 'No agent context files found',
184
+ fixable: false,
185
+ });
186
+ return {
187
+ name: 'context',
188
+ score: Math.max(0, score),
189
+ maxScore: 100,
190
+ issues,
191
+ summary: 'no agent context files found',
192
+ };
193
+ }
194
+ const allSections = [];
195
+ let totalTokens = 0;
196
+ for (const filePath of files) {
197
+ const content = cachedReadFile(filePath);
198
+ if (!content)
199
+ continue;
200
+ const relPath = filePath.startsWith(cwd) ? filePath.slice(cwd.length + 1) : filePath;
201
+ const sections = splitIntoSections(content, relPath);
202
+ allSections.push(...sections);
203
+ const fileTokens = sections.reduce((sum, s) => sum + s.tokens, 0);
204
+ totalTokens += fileTokens;
205
+ if (fileTokens > BLOATED_FILE_THRESHOLD) {
206
+ issues.push({
207
+ severity: 'warning',
208
+ message: `Context file exceeds 10K tokens: ${relPath} (${fileTokens} tokens)`,
209
+ file: relPath,
210
+ fixable: false,
211
+ fixHint: 'Split or trim this file to reduce token cost',
212
+ });
213
+ score -= 5;
214
+ }
215
+ }
216
+ // Stale detection
217
+ const staleSections = detectStaleSections(allSections);
218
+ let staleDeduction = 0;
219
+ let staleSavings = 0;
220
+ for (const key of staleSections) {
221
+ const [file, title] = key.split('::');
222
+ const section = allSections.find(s => s.file === file && s.title === title);
223
+ if (section)
224
+ staleSavings += section.tokens;
225
+ if (staleDeduction < 40) {
226
+ issues.push({
227
+ severity: 'warning',
228
+ message: `Stale section: "${title}" in ${file}`,
229
+ file,
230
+ fixable: false,
231
+ fixHint: 'Section not referenced in recent sessions — consider removing',
232
+ });
233
+ staleDeduction += 10;
234
+ }
235
+ }
236
+ score -= Math.min(staleDeduction, 40);
237
+ // Token threshold penalty
238
+ if (totalTokens > TOKEN_THRESHOLD) {
239
+ const over = totalTokens - TOKEN_THRESHOLD;
240
+ const penalty = Math.min(Math.floor(over / 2000) * 5, 30);
241
+ score -= penalty;
242
+ }
243
+ // Info issues
244
+ issues.push({
245
+ severity: 'info',
246
+ message: `Total context: ${totalTokens} tokens across ${files.length} file${files.length !== 1 ? 's' : ''}`,
247
+ fixable: false,
248
+ });
249
+ if (staleSavings > 0) {
250
+ const savings = formatCost(calculateCost(staleSavings, 'sonnet'));
251
+ issues.push({
252
+ severity: 'info',
253
+ message: `Potential savings: ${staleSavings} tokens (${savings}/call at sonnet rates) from removing stale sections`,
254
+ fixable: false,
255
+ });
256
+ }
257
+ return {
258
+ name: 'context',
259
+ score: Math.max(0, score),
260
+ maxScore: 100,
261
+ issues,
262
+ summary: `${totalTokens} tokens in ${files.length} context file${files.length !== 1 ? 's' : ''}${staleSections.size > 0 ? `, ${staleSections.size} stale section${staleSections.size !== 1 ? 's' : ''}` : ''}`,
263
+ };
264
+ }
265
+ // ── Subcommand output ────────────────────────────────────────────────────────
266
+ export async function runContextCommand(format, cwd) {
267
+ const files = discoverContextFiles(cwd);
268
+ if (files.length === 0) {
269
+ if (format === 'json') {
270
+ console.log(JSON.stringify({ files: [], sections: [], totalTokens: 0, costs: {}, stale: [], score: 0 }, null, 2));
271
+ }
272
+ else {
273
+ console.log(`\n ${c.bold}vet context${c.reset} — no agent context files found\n`);
274
+ }
275
+ return;
276
+ }
277
+ const allSections = [];
278
+ let totalTokens = 0;
279
+ for (const filePath of files) {
280
+ const content = cachedReadFile(filePath);
281
+ if (!content)
282
+ continue;
283
+ const relPath = filePath.startsWith(cwd) ? filePath.slice(cwd.length + 1) : filePath;
284
+ const sections = splitIntoSections(content, relPath);
285
+ allSections.push(...sections);
286
+ totalTokens += sections.reduce((sum, s) => sum + s.tokens, 0);
287
+ }
288
+ const staleSections = detectStaleSections(allSections);
289
+ if (format === 'json') {
290
+ const result = {
291
+ files: files.map(f => f.startsWith(cwd) ? f.slice(cwd.length + 1) : f),
292
+ sections: allSections.map(s => ({
293
+ file: s.file,
294
+ title: s.title,
295
+ tokens: s.tokens,
296
+ stale: staleSections.has(`${s.file}::${s.title}`),
297
+ costs: {
298
+ opus: calculateCost(s.tokens, 'opus'),
299
+ sonnet: calculateCost(s.tokens, 'sonnet'),
300
+ haiku: calculateCost(s.tokens, 'haiku'),
301
+ },
302
+ })),
303
+ totalTokens,
304
+ costs: {
305
+ opus: calculateCost(totalTokens, 'opus'),
306
+ sonnet: calculateCost(totalTokens, 'sonnet'),
307
+ haiku: calculateCost(totalTokens, 'haiku'),
308
+ },
309
+ stale: [...staleSections],
310
+ staleSavingsTokens: allSections
311
+ .filter(s => staleSections.has(`${s.file}::${s.title}`))
312
+ .reduce((sum, s) => sum + s.tokens, 0),
313
+ };
314
+ console.log(JSON.stringify(result, null, 2));
315
+ return;
316
+ }
317
+ // ASCII table output
318
+ console.log(`\n ${c.bold}vet context${c.reset} — agent context cost audit\n`);
319
+ // Header
320
+ const fileW = 30;
321
+ const sectionW = 25;
322
+ const tokenW = 8;
323
+ const costW = 12;
324
+ console.log(` ${c.dim}${'─'.repeat(fileW + sectionW + tokenW + costW * 3 + 10)}${c.reset}`);
325
+ console.log(` ${pad('File', fileW)} ${pad('Section', sectionW)} ${padR('Tokens', tokenW)} ${padR('Opus', costW)} ${padR('Sonnet', costW)} ${padR('Haiku', costW)}`);
326
+ console.log(` ${c.dim}${'─'.repeat(fileW + sectionW + tokenW + costW * 3 + 10)}${c.reset}`);
327
+ for (const s of allSections) {
328
+ const isStale = staleSections.has(`${s.file}::${s.title}`);
329
+ const staleMarker = isStale ? ` ${c.yellow}⚠ stale${c.reset}` : '';
330
+ const file = truncate(s.file, fileW);
331
+ const title = truncate(s.title, sectionW);
332
+ console.log(` ${pad(file, fileW)} ${pad(title, sectionW)} ${padR(String(s.tokens), tokenW)} ${padR(formatCost(calculateCost(s.tokens, 'opus')), costW)} ${padR(formatCost(calculateCost(s.tokens, 'sonnet')), costW)} ${padR(formatCost(calculateCost(s.tokens, 'haiku')), costW)}${staleMarker}`);
333
+ }
334
+ console.log(` ${c.dim}${'─'.repeat(fileW + sectionW + tokenW + costW * 3 + 10)}${c.reset}`);
335
+ console.log(` ${pad(c.bold + 'Total' + c.reset, fileW)} ${pad('', sectionW)} ${padR(String(totalTokens), tokenW)} ${padR(formatCost(calculateCost(totalTokens, 'opus')), costW)} ${padR(formatCost(calculateCost(totalTokens, 'sonnet')), costW)} ${padR(formatCost(calculateCost(totalTokens, 'haiku')), costW)}`);
336
+ console.log('');
337
+ if (staleSections.size > 0) {
338
+ const staleToks = allSections
339
+ .filter(s => staleSections.has(`${s.file}::${s.title}`))
340
+ .reduce((sum, s) => sum + s.tokens, 0);
341
+ console.log(` ${c.yellow}⚠ ${staleSections.size} stale section${staleSections.size !== 1 ? 's' : ''} detected${c.reset} — ${staleToks} tokens (${formatCost(calculateCost(staleToks, 'sonnet'))}/call at sonnet rates)`);
342
+ console.log(` ${c.dim}These sections weren't referenced in recent Claude sessions${c.reset}\n`);
343
+ }
344
+ }
345
+ // ── String helpers ───────────────────────────────────────────────────────────
346
+ function pad(s, w) {
347
+ // Strip ANSI for length calculation
348
+ const clean = s.replace(/\x1b\[[0-9;]*m/g, '');
349
+ return s + ' '.repeat(Math.max(0, w - clean.length));
350
+ }
351
+ function padR(s, w) {
352
+ const clean = s.replace(/\x1b\[[0-9;]*m/g, '');
353
+ return ' '.repeat(Math.max(0, w - clean.length)) + s;
354
+ }
355
+ function truncate(s, max) {
356
+ if (s.length <= max)
357
+ return s;
358
+ return s.slice(0, max - 1) + '…';
359
+ }
@@ -26,10 +26,16 @@ function analyzeCatch(node) {
26
26
  const block = node.block;
27
27
  const stmts = block.statements;
28
28
  const line = node.getSourceFile().getLineAndCharacterOfPosition(node.getStart()).line + 1;
29
+ const text = block.getText();
29
30
  if (stmts.length === 0) {
31
+ // Check if there's a deliberate comment (/* skip */, /* ignore */, etc.)
32
+ const hasComment = /\/[/*]\s*(skip|ignore|noop|intentional|expected|ok|no-op)/i.test(text);
33
+ if (hasComment) {
34
+ // Deliberate empty catch — not a bug
35
+ return { line, isEmpty: false, isLazy: false, isRethrow: false };
36
+ }
30
37
  return { line, isEmpty: true, isLazy: false, isRethrow: false };
31
38
  }
32
- const text = block.getText();
33
39
  const isLazy = stmts.length === 1 && /console\.(log|error|warn)\s*\(/.test(text) && !text.includes('throw');
34
40
  const isRethrow = text.includes('throw');
35
41
  return { line, isEmpty: false, isLazy, isRethrow };
@@ -311,6 +311,16 @@ const TOOLING_PACKAGES = new Set([
311
311
  'del-cli', 'make-node',
312
312
  // Type packages (consumed by TS compiler, not imported)
313
313
  '@types/react', '@types/react-dom', '@types/jest', '@types/mocha',
314
+ // Test runners / e2e (used via CLI, not imported)
315
+ 'playwright', '@playwright/test', 'cypress', 'puppeteer',
316
+ // Package quality tools (used via CLI)
317
+ 'publint', 'arethetypeswrong', 'are-the-types-wrong', 'attw',
318
+ 'pkg-pr-new', 'size-limit', '@size-limit/preset-small-lib',
319
+ // Monorepo/workspace tools
320
+ 'update-ts-references', 'syncpack', 'manypkg',
321
+ // Prettier plugins (loaded via config, not imported)
322
+ 'prettier-plugin-svelte', 'prettier-plugin-tailwindcss',
323
+ 'prettier-plugin-organize-imports', 'prettier-plugin-packagejson',
314
324
  ]);
315
325
  // ── Collect all deps declared in workspace sub-packages ──────────────────────
316
326
  export function collectWorkspaceDeps(cwd) {
@@ -500,6 +510,11 @@ export async function checkDeps(cwd) {
500
510
  // Skip known tooling packages that are devDependencies (used via CLI scripts, not imports)
501
511
  if (TOOLING_PACKAGES.has(pkg) && devDepNames.has(pkg))
502
512
  continue;
513
+ // Wildcard tooling patterns (eslint configs, prettier plugins, @types/*)
514
+ if (devDepNames.has(pkg) && (pkg.startsWith('eslint-config-') || pkg.startsWith('eslint-plugin-') ||
515
+ pkg.startsWith('prettier-plugin-') || pkg.startsWith('@types/') ||
516
+ pkg.startsWith('@typescript-eslint/') || pkg.startsWith('@eslint/')))
517
+ continue;
503
518
  // Check if it's a CLI tool / plugin / type package (common false positives)
504
519
  // Still flag it, but as info
505
520
  issues.push({
@@ -130,7 +130,7 @@ export async function checkSemantic(cwd) {
130
130
  patternEmbeddings.push({ pattern, embedding: new Float32Array(result.data) });
131
131
  }
132
132
  // Embed and compare each function
133
- const THRESHOLD = 0.40; // similarity threshold — code-to-code embeddings
133
+ const THRESHOLD = 0.45; // similarity threshold — code-to-code embeddings (0.40 gave false positives)
134
134
  for (const func of funcsToAnalyze) {
135
135
  const result = await extractor(func.body, { pooling: 'mean', normalize: true });
136
136
  const funcEmb = new Float32Array(result.data);
package/dist/cli.js CHANGED
@@ -31,6 +31,7 @@ import { checkLoop, runLoopCommand } from './checks/loop.js';
31
31
  import { checkBloat, runBloatCommand } from './checks/bloat.js';
32
32
  import { checkGuard, runGuardCommand } from './checks/guard.js';
33
33
  import { checkExplain, runExplainCommand } from './checks/explain.js';
34
+ import { checkContext, runContextCommand } from './checks/context.js';
34
35
  import { checkCompleteness } from './checks/completeness.js';
35
36
  import { score } from './scorer.js';
36
37
  import { reportPretty, reportJSON, reportBadge } from './reporter.js';
@@ -86,6 +87,7 @@ if (flags.has('--help') || flags.has('-h')) {
86
87
  npx @safetnsr/vet bloat detect agent-generated code bloat
87
88
  npx @safetnsr/vet guard [dir] scan for destructive operation bomb sites
88
89
  npx @safetnsr/vet explain [--since REF] [--verbose] [--json] risk-tier agent changes
90
+ npx @safetnsr/vet context [dir] audit agent context files for token cost + stale sections
89
91
 
90
92
  ${c.dim}categories:${c.reset}
91
93
  security (30%) scan, secrets, config, model usage
@@ -121,7 +123,7 @@ if (flags.has('--version') || flags.has('-v')) {
121
123
  }
122
124
  process.exit(0);
123
125
  }
124
- const COMMANDS = ['init', 'receipt', 'map', 'permissions', 'compact', 'subsidy', 'loop', 'bloat', 'guard', 'explain'];
126
+ const COMMANDS = ['init', 'receipt', 'map', 'permissions', 'compact', 'subsidy', 'loop', 'bloat', 'guard', 'explain', 'context'];
125
127
  const command = COMMANDS.includes(positional[0]) ? positional[0] : undefined;
126
128
  const cwd = resolve(positional.find(p => !COMMANDS.includes(p)) || '.');
127
129
  const isCI = flags.has('--ci');
@@ -263,6 +265,17 @@ if (command === 'guard') {
263
265
  }
264
266
  process.exit(0);
265
267
  }
268
+ if (command === 'context') {
269
+ try {
270
+ const format = isJSON ? 'json' : 'ascii';
271
+ await runContextCommand(format, cwd);
272
+ }
273
+ catch (e) {
274
+ console.error(`${c.red}context failed:${c.reset}`, e instanceof Error ? e.message : e);
275
+ process.exit(1);
276
+ }
277
+ process.exit(0);
278
+ }
266
279
  if (command === 'explain') {
267
280
  try {
268
281
  const format = isJSON ? 'json' : 'ascii';
@@ -327,7 +340,7 @@ async function runChecks() {
327
340
  }
328
341
  }
329
342
  // Run ALL independent checks in parallel
330
- const [scanResult, secretsResult, configResult, modelsResult, owaspResult, permissionsResult, integrityResult, readyResult, debtResult, depsResult, receiptResult, compactResult, subsidyResult, memoryResult, verifyResult, testsResult, loopResult, completenessResult, bloatResult, guardResult, explainResult, architectureResult, aireadyResult, deepResult, semanticResult, hotspotsResult, clonesResult,] = await Promise.all([
343
+ const [scanResult, secretsResult, configResult, modelsResult, owaspResult, permissionsResult, integrityResult, readyResult, debtResult, depsResult, receiptResult, compactResult, subsidyResult, memoryResult, verifyResult, testsResult, loopResult, completenessResult, bloatResult, guardResult, explainResult, architectureResult, aireadyResult, deepResult, semanticResult, hotspotsResult, clonesResult, contextResult,] = await Promise.all([
331
344
  withTimeout('scan', () => checkScan(cwd)),
332
345
  withTimeout('secrets', () => checkSecrets(cwd)),
333
346
  withTimeout('config', () => checkConfig(cwd, ignore)),
@@ -355,6 +368,7 @@ async function runChecks() {
355
368
  withTimeout('semantic', () => checkSemantic(cwd), 60_000),
356
369
  withTimeout('hotspots', () => checkHotspots(cwd), 30_000),
357
370
  withTimeout('clones', () => checkClones(cwd), 60_000),
371
+ withTimeout('context', () => checkContext(cwd)),
358
372
  ]);
359
373
  // Git-dependent checks (diff + history) — parallel with each other
360
374
  const [diffResult, historyResult] = await Promise.all([
@@ -369,7 +383,7 @@ async function runChecks() {
369
383
  debt: [readyResult, historyResult, debtResult, bloatResult, clonesResult],
370
384
  deps: [depsResult],
371
385
  architecture: [architectureResult],
372
- aiready: [aireadyResult, deepResult, semanticResult],
386
+ aiready: [aireadyResult, deepResult, semanticResult, contextResult],
373
387
  history: [hotspotsResult],
374
388
  });
375
389
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@safetnsr/vet",
3
- "version": "1.20.0",
3
+ "version": "1.21.0",
4
4
  "description": "vet your AI-generated code — one command, one score card, one letter grade",
5
5
  "type": "module",
6
6
  "bin": {
@@ -44,6 +44,7 @@
44
44
  "@safetnsr/model-graveyard": "^0.2.0"
45
45
  },
46
46
  "dependencies": {
47
- "@huggingface/transformers": "^3.8.1"
47
+ "@huggingface/transformers": "^3.8.1",
48
+ "js-tiktoken": "^1.0.21"
48
49
  }
49
50
  }