@orxataguy/tyr 1.0.32 → 1.0.33
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/package.json
CHANGED
|
@@ -16,6 +16,7 @@ const CONTEXT_FILENAMES = [
|
|
|
16
16
|
'CLAUDE.md',
|
|
17
17
|
'AGENTS.md',
|
|
18
18
|
'CONTEXT.md',
|
|
19
|
+
'README.md',
|
|
19
20
|
'.cursorrules',
|
|
20
21
|
path.join('.github', 'copilot-instructions.md'),
|
|
21
22
|
];
|
|
@@ -26,6 +27,16 @@ const SNAPSHOT_TREE_MAX_DEPTH = getEnvInt('SNAPSHOT_TREE_MAX_DEPTH', 5);
|
|
|
26
27
|
const SNAPSHOT_TREE_MAX_ENTRIES_PER_DIR = getEnvInt('SNAPSHOT_TREE_MAX_ENTRIES_PER_DIR', 250);
|
|
27
28
|
const SNAPSHOT_SECTION_MAX_CHARS = getEnvInt('SNAPSHOT_SECTION_MAX_CHARS', 40000);
|
|
28
29
|
|
|
30
|
+
// How far findContextFiles() looks for guideline files OUTSIDE the exact target directory: upward
|
|
31
|
+
// into ancestor directories (toward the repo root — a monorepo root AGENTS.md still applies to a
|
|
32
|
+
// package inside it) and downward into subdirectories (a nested package may document itself).
|
|
33
|
+
// Both are bounded so a huge repo doesn't turn every command into a full filesystem crawl.
|
|
34
|
+
const CONTEXT_SEARCH_UPWARD_LEVELS = getEnvInt('CONTEXT_SEARCH_UPWARD_LEVELS', 4);
|
|
35
|
+
const CONTEXT_SEARCH_DOWNWARD_LEVELS = getEnvInt('CONTEXT_SEARCH_DOWNWARD_LEVELS', 3);
|
|
36
|
+
// Cap on how many context files are actually used, once found — closest to `dir` wins. Keeps a
|
|
37
|
+
// monorepo with many nested AGENTS.md files from flooding the prompt with everything it can reach.
|
|
38
|
+
const MAX_CONTEXT_FILES = getEnvInt('MAX_CONTEXT_FILES', 6);
|
|
39
|
+
|
|
29
40
|
const DEFAULT_IGNORED_DIRS = new Set<string>([
|
|
30
41
|
'node_modules', '.git', '.hg', '.svn',
|
|
31
42
|
'.turbo', '.next', '.nuxt', '.cache', '.parcel-cache',
|
|
@@ -315,15 +326,85 @@ export class AIContextManager {
|
|
|
315
326
|
|
|
316
327
|
/**
|
|
317
328
|
* @method findContextFiles
|
|
318
|
-
* @description Busca los archivos de directrices conocidos
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
*
|
|
329
|
+
* @description Busca los archivos de directrices conocidos (README.md, CLAUDE.md, AGENTS.md,
|
|
330
|
+
* CONTEXT.md, .cursorrules, copilot-instructions.md) no solo dentro de `dir`, sino tanto hacia
|
|
331
|
+
* fuera (directorios ancestros, hasta CONTEXT_SEARCH_UPWARD_LEVELS niveles — un AGENTS.md en la
|
|
332
|
+
* raíz de un monorepo sigue aplicando a un paquete dentro de él) como hacia dentro (subcarpetas,
|
|
333
|
+
* hasta CONTEXT_SEARCH_DOWNWARD_LEVELS niveles — un paquete anidado puede documentarse a sí
|
|
334
|
+
* mismo). La "relevancia" se determina de forma determinista, sin llamar a la IA (esto se
|
|
335
|
+
* ejecuta antes de cada comando, así que tiene que ser barato):
|
|
336
|
+
* - Hacia fuera: se detiene en cuanto se cruza la raíz del repo (una carpeta .git), además del
|
|
337
|
+
* tope de niveles — más allá de eso nunca es relevante.
|
|
338
|
+
* - Hacia dentro: NO se desciende más allá del primer directorio que tenga su propio
|
|
339
|
+
* package.json/composer.json (un límite de paquete independiente) salvo para recoger el
|
|
340
|
+
* archivo de contexto que esté justo en ese límite; lo que hay más adentro pertenece a ese
|
|
341
|
+
* otro paquete, no a `dir`.
|
|
342
|
+
* - Si aun así se encuentran más de MAX_CONTEXT_FILES candidatos, ganan los más cercanos a `dir`.
|
|
343
|
+
* @param {string} dir - Ruta absoluta a la raíz del proyecto (o subcarpeta) desde la que buscar.
|
|
344
|
+
* @returns {Promise<string[]>} Rutas absolutas de los archivos de directrices relevantes, del más al menos cercano.
|
|
322
345
|
*/
|
|
323
|
-
public findContextFiles(dir: string): string[] {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
346
|
+
public async findContextFiles(dir: string): Promise<string[]> {
|
|
347
|
+
const found = new Map<string, number>();
|
|
348
|
+
|
|
349
|
+
const record = (fullPath: string, distance: number) => {
|
|
350
|
+
if (!this.fs.exists(fullPath)) return;
|
|
351
|
+
const previous = found.get(fullPath);
|
|
352
|
+
if (previous === undefined || distance < previous) found.set(fullPath, distance);
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
// Here (distance 0).
|
|
356
|
+
for (const name of CONTEXT_FILENAMES) record(path.join(dir, name), 0);
|
|
357
|
+
|
|
358
|
+
// Outward: ancestor directories, stopping at the repo root once reached.
|
|
359
|
+
let upDir = dir;
|
|
360
|
+
for (let level = 1; level <= CONTEXT_SEARCH_UPWARD_LEVELS; level++) {
|
|
361
|
+
const parent = path.dirname(upDir);
|
|
362
|
+
if (parent === upDir) break; // filesystem root
|
|
363
|
+
upDir = parent;
|
|
364
|
+
for (const name of CONTEXT_FILENAMES) record(path.join(upDir, name), level);
|
|
365
|
+
if (this.fs.exists(path.join(upDir, '.git'))) break;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Inward: subdirectories, pruned at package boundaries.
|
|
369
|
+
await this.walkDownForContextFiles(dir, 1, false, record);
|
|
370
|
+
|
|
371
|
+
return [...found.entries()]
|
|
372
|
+
.sort((a, b) => a[1] - b[1])
|
|
373
|
+
.slice(0, MAX_CONTEXT_FILES)
|
|
374
|
+
.map(([fullPath]) => fullPath);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** Recursive helper for the inward half of findContextFiles(). `crossedPackageBoundary` starts
|
|
378
|
+
* false and flips to true once we step into a directory with its own manifest — from that
|
|
379
|
+
* point on we keep recursing (so deeper package boundaries are still detected and stopped at
|
|
380
|
+
* in turn) but stop *recording* candidates, since anything past that boundary belongs to a
|
|
381
|
+
* separately versioned sub-package rather than `dir`. */
|
|
382
|
+
private async walkDownForContextFiles(
|
|
383
|
+
currentDir: string,
|
|
384
|
+
level: number,
|
|
385
|
+
crossedPackageBoundary: boolean,
|
|
386
|
+
record: (fullPath: string, distance: number) => void
|
|
387
|
+
): Promise<void> {
|
|
388
|
+
if (level > CONTEXT_SEARCH_DOWNWARD_LEVELS) return;
|
|
389
|
+
|
|
390
|
+
let entries;
|
|
391
|
+
try {
|
|
392
|
+
entries = await this.fs.readdir(currentDir, { withFileTypes: true });
|
|
393
|
+
} catch {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
for (const entry of entries) {
|
|
398
|
+
if (!entry.isDirectory() || DEFAULT_IGNORED_DIRS.has(entry.name)) continue;
|
|
399
|
+
const childDir = path.join(currentDir, entry.name);
|
|
400
|
+
|
|
401
|
+
if (!crossedPackageBoundary) {
|
|
402
|
+
for (const name of CONTEXT_FILENAMES) record(path.join(childDir, name), level);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const childHasOwnManifest = [...MANIFEST_FILENAMES].some(name => this.fs.exists(path.join(childDir, name)));
|
|
406
|
+
await this.walkDownForContextFiles(childDir, level + 1, crossedPackageBoundary || childHasOwnManifest, record);
|
|
407
|
+
}
|
|
327
408
|
}
|
|
328
409
|
|
|
329
410
|
private async readAndValidate(filePath: string): Promise<string | null> {
|
|
@@ -483,7 +564,7 @@ export class AIContextManager {
|
|
|
483
564
|
* @returns {Promise<GuidelinesBlock[]>}
|
|
484
565
|
*/
|
|
485
566
|
public async readGuidelines(dir: string): Promise<GuidelinesBlock[]> {
|
|
486
|
-
let files = this.findContextFiles(dir);
|
|
567
|
+
let files = await this.findContextFiles(dir);
|
|
487
568
|
|
|
488
569
|
if (files.length === 0) {
|
|
489
570
|
const generated = await this.generateContextFile(dir);
|
|
@@ -493,7 +574,10 @@ export class AIContextManager {
|
|
|
493
574
|
const blocks: GuidelinesBlock[] = [];
|
|
494
575
|
for (const file of files) {
|
|
495
576
|
const content = await this.readAndValidate(file);
|
|
496
|
-
|
|
577
|
+
// Relative to `dir` rather than just the basename: with the search now reaching
|
|
578
|
+
// outward/inward, two blocks can share a filename (e.g. an AGENTS.md here AND one two
|
|
579
|
+
// levels up) — the path disambiguates which is which for whoever reads getGuidelinesText().
|
|
580
|
+
if (content) blocks.push({ fileName: path.relative(dir, file) || path.basename(file), content });
|
|
497
581
|
}
|
|
498
582
|
|
|
499
583
|
if (blocks.length === 0) {
|
|
@@ -132,14 +132,14 @@ type ModelTier = 'cheap' | 'balanced' | 'flagship';
|
|
|
132
132
|
|
|
133
133
|
const MODEL_TIERS: Record<AIVendor, Record<ModelTier, string>> = {
|
|
134
134
|
anthropic: { cheap: 'claude-haiku-4-5', balanced: 'claude-sonnet-5', flagship: 'claude-opus-4-1' },
|
|
135
|
-
openai: { cheap: 'gpt-4o-mini', balanced: 'gpt-4o', flagship: 'gpt-4o' },
|
|
135
|
+
openai: { cheap: 'gpt-4o-mini', balanced: 'gpt-4o-mini', flagship: 'gpt-4o' },
|
|
136
136
|
gemini: { cheap: 'gemini-2.5-flash', balanced: 'gemini-2.5-pro', flagship: 'gemini-2.5-pro' },
|
|
137
137
|
};
|
|
138
138
|
|
|
139
|
-
const MODEL_TIER_ENV: Record<ModelTier, string
|
|
140
|
-
cheap: '
|
|
141
|
-
balanced: '
|
|
142
|
-
flagship: '
|
|
139
|
+
const MODEL_TIER_ENV: Record<AIVendor, Record<ModelTier, string>> = {
|
|
140
|
+
anthropic: { cheap: 'ANTHROPIC_AI_MODEL_CHEAP', balanced: 'ANTHROPIC_AI_MODEL_BALANCED', flagship: 'ANTHROPIC_AI_MODEL_FLAGSHIP' },
|
|
141
|
+
openai: { cheap: 'OPENAI_AI_MODEL_CHEAP', balanced: 'OPENAI_AI_MODEL_BALANCED', flagship: 'OPENAI_AI_MODEL_FLAGSHIP' },
|
|
142
|
+
gemini: { cheap: 'GEMINI_AI_MODEL_CHEAP', balanced: 'GEMINI_AI_MODEL_BALANCED', flagship: 'GEMINI_AI_MODEL_FLAGSHIP' },
|
|
143
143
|
};
|
|
144
144
|
|
|
145
145
|
/** budget_tokens sent to Anthropic's `thinking` param per effort level. 'none' disables thinking. */
|
|
@@ -260,7 +260,7 @@ export class AIVendorManager {
|
|
|
260
260
|
throw new TyrError(`Unknown task priority: '${priority}'`, null, `Use one of: ${PRIORITY_LADDER.join(', ')}`);
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
-
const tierModel = getEnvString(MODEL_TIER_ENV[route.tier]) ?? MODEL_TIERS[vendor][route.tier];
|
|
263
|
+
const tierModel = getEnvString(MODEL_TIER_ENV[vendor][route.tier]) ?? MODEL_TIERS[vendor][route.tier];
|
|
264
264
|
|
|
265
265
|
return {
|
|
266
266
|
vendor,
|