@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orxataguy/tyr",
3
- "version": "1.0.32",
3
+ "version": "1.0.33",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "tyr": "./bin/tyr.js"
@@ -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 directamente dentro del directorio
319
- * del proyecto.
320
- * @param {string} dir - Ruta absoluta a la raíz del proyecto.
321
- * @returns {string[]} Rutas absolutas de los archivos de directrices que existen.
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
- return CONTEXT_FILENAMES
325
- .map(name => path.join(dir, name))
326
- .filter(fullPath => this.fs.exists(fullPath));
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
- if (content) blocks.push({ fileName: path.basename(file), content });
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: 'AI_MODEL_CHEAP',
141
- balanced: 'AI_MODEL_BALANCED',
142
- flagship: 'AI_MODEL_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,