@orxataguy/tyr 1.0.29 → 1.0.31
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/README.md +19 -10
- package/package.json +1 -1
- package/src/core/Container.ts +14 -0
- package/src/core/sys/config.ts +11 -2
- package/src/core/util/getenv.ts +57 -0
- package/src/lib/AIContextManager.ts +302 -0
- package/src/lib/AIVendorManager.ts +646 -0
- package/src/lib/FileSystemManager.ts +22 -2
- package/src/lib/JiraManager.ts +5 -2
- package/src/lib/MongoManager.ts +3 -2
- package/src/lib/PromptTemplateManager.ts +142 -0
- package/src/lib/SQLManager.ts +5 -4
- package/src/lib/TokenManager.ts +169 -0
package/README.md
CHANGED
|
@@ -252,6 +252,8 @@ tyr deploy --debug
|
|
|
252
252
|
│ │ ├── Kernel.ts # Command router and execution engine
|
|
253
253
|
│ │ ├── Container.ts # Dependency injection container
|
|
254
254
|
│ │ ├── TyrError.ts # Structured error type
|
|
255
|
+
│ │ ├── util
|
|
256
|
+
│ │ │ └── getenv.ts # Helper: get environment variables
|
|
255
257
|
│ │ └── sys/
|
|
256
258
|
│ │ ├── gen.ts # Built-in: scaffold a command
|
|
257
259
|
│ │ ├── rem.ts # Built-in: remove a command
|
|
@@ -259,16 +261,23 @@ tyr deploy --debug
|
|
|
259
261
|
│ ├── commands/
|
|
260
262
|
│ │ └── *.tyr.ts # Your custom commands go here
|
|
261
263
|
│ └── lib/
|
|
262
|
-
│
|
|
263
|
-
│
|
|
264
|
-
│
|
|
265
|
-
│
|
|
266
|
-
│
|
|
267
|
-
│
|
|
268
|
-
│
|
|
269
|
-
│
|
|
270
|
-
├──
|
|
271
|
-
│
|
|
264
|
+
│ │ ├── AIContextManager.ts
|
|
265
|
+
│ │ ├── AIVendorManager.ts
|
|
266
|
+
│ │ ├── DockerManager.ts
|
|
267
|
+
│ │ ├── FileSystemManager.ts
|
|
268
|
+
│ │ ├── GitManager.ts
|
|
269
|
+
│ │ ├── JiraManager.ts
|
|
270
|
+
│ │ ├── MongoManager.ts
|
|
271
|
+
│ │ ├── PackageManager.ts
|
|
272
|
+
│ │ ├── PromptTemplateManager.ts
|
|
273
|
+
│ │ ├── SetupManager.ts
|
|
274
|
+
│ │ ├── ShellManager.ts
|
|
275
|
+
│ │ ├── SQLManager.ts
|
|
276
|
+
│ │ ├── SystemManager.ts
|
|
277
|
+
│ │ ├── TokenManager.ts
|
|
278
|
+
│ │ ├── WebManager.ts
|
|
279
|
+
│ │ └── WorkspaceManager.ts
|
|
280
|
+
| └── index.ts # Exports context
|
|
272
281
|
└── tests/
|
|
273
282
|
├── setup.ts # Mock context factory
|
|
274
283
|
└── test-runner.ts # Smoke test runner
|
package/package.json
CHANGED
package/src/core/Container.ts
CHANGED
|
@@ -10,6 +10,10 @@ import { WebManager } from '../lib/WebManager.js';
|
|
|
10
10
|
import { WorkspaceManager } from '../lib/WorkspaceManager.js';
|
|
11
11
|
import { JiraManager } from '../lib/JiraManager.js';
|
|
12
12
|
import { SetupManager } from '../lib/SetupManager.js';
|
|
13
|
+
import { AIVendorManager } from '../lib/AIVendorManager.js';
|
|
14
|
+
import { AIContextManager } from '../lib/AIContextManager.js';
|
|
15
|
+
import { PromptTemplateManager } from '../lib/PromptTemplateManager.js';
|
|
16
|
+
import { TokenManager } from '../lib/TokenManager.js';
|
|
13
17
|
import { Logger, createLogger } from './Logger.js';
|
|
14
18
|
|
|
15
19
|
import path from 'path';
|
|
@@ -31,6 +35,10 @@ export interface ServiceContainer {
|
|
|
31
35
|
workspace: WorkspaceManager;
|
|
32
36
|
jira: JiraManager;
|
|
33
37
|
setup: SetupManager;
|
|
38
|
+
aiVendor: AIVendorManager;
|
|
39
|
+
aiContext: AIContextManager;
|
|
40
|
+
prompts: PromptTemplateManager;
|
|
41
|
+
tokens: TokenManager;
|
|
34
42
|
}
|
|
35
43
|
|
|
36
44
|
export class Container {
|
|
@@ -47,6 +55,8 @@ export class Container {
|
|
|
47
55
|
const mongo = new MongoManager();
|
|
48
56
|
const web = new WebManager(logger);
|
|
49
57
|
const fs = new FileSystemManager(logger);
|
|
58
|
+
const aiVendor = new AIVendorManager(logger);
|
|
59
|
+
const aiContext = new AIContextManager(fs, aiVendor, logger);
|
|
50
60
|
|
|
51
61
|
this.services = {
|
|
52
62
|
logger,
|
|
@@ -63,6 +73,10 @@ export class Container {
|
|
|
63
73
|
workspace: new WorkspaceManager(shell, fs, logger),
|
|
64
74
|
jira: new JiraManager(web, shell, logger),
|
|
65
75
|
setup: new SetupManager(shell, fs, logger),
|
|
76
|
+
aiVendor,
|
|
77
|
+
aiContext,
|
|
78
|
+
prompts: new PromptTemplateManager(aiContext, logger),
|
|
79
|
+
tokens: new TokenManager(logger),
|
|
66
80
|
};
|
|
67
81
|
}
|
|
68
82
|
|
package/src/core/sys/config.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
+
import { getEnvString } from '../util/getenv.js';
|
|
2
3
|
import { homedir, platform } from 'os';
|
|
3
4
|
import { existsSync, cpSync, rmSync, mkdirSync, readdirSync } from 'fs';
|
|
4
5
|
import { execSync } from 'child_process';
|
|
@@ -48,7 +49,7 @@ function backupUserRoot(userRoot: string, backupPath: string): void {
|
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
function detectShellRcFile(homeDir: string): string | null {
|
|
51
|
-
const shell =
|
|
52
|
+
const shell = getEnvString('SHELL', '');
|
|
52
53
|
if (shell.includes('zsh')) return path.join(homeDir, '.zshrc');
|
|
53
54
|
if (shell.includes('fish')) return path.join(homeDir, '.config', 'fish', 'config.fish');
|
|
54
55
|
if (shell.includes('bash')) {
|
|
@@ -60,7 +61,7 @@ function detectShellRcFile(homeDir: string): string | null {
|
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
function getWindowsProfilePaths(): string[] {
|
|
63
|
-
const userProfile =
|
|
64
|
+
const userProfile = getEnvString('USERPROFILE');
|
|
64
65
|
if (!userProfile) return [];
|
|
65
66
|
return [
|
|
66
67
|
path.join(userProfile, 'Documents', 'WindowsPowerShell', 'Microsoft.PowerShell_profile.ps1'),
|
|
@@ -123,6 +124,14 @@ MSSQL_DATABASE=
|
|
|
123
124
|
# MongoDB database
|
|
124
125
|
MONGO_URI=
|
|
125
126
|
MONGO_DATABASE=
|
|
127
|
+
# AI vendor (anthropic | openai | gemini)
|
|
128
|
+
AI_VENDOR=anthropic
|
|
129
|
+
ANTHROPIC_API_KEY=
|
|
130
|
+
OPENAI_API_KEY=
|
|
131
|
+
GEMINI_API_KEY=
|
|
132
|
+
AI_MODEL=
|
|
133
|
+
AI_TEMPERATURE=
|
|
134
|
+
AI_MAX_TOKENS=
|
|
126
135
|
`;
|
|
127
136
|
|
|
128
137
|
const SH_ALIASES_TEMPLATE = `# Add your custom aliases here.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
|
|
2
|
+
export function getEnvString(name: string, fallback: string): string;
|
|
3
|
+
export function getEnvString(name: string, fallback?: string): string | undefined;
|
|
4
|
+
export function getEnvString(name: string, fallback?: string): string | undefined {
|
|
5
|
+
const value = process.env[name];
|
|
6
|
+
return value === undefined || value === '' ? fallback : value;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
export function getEnvInt(name: string, fallback: number): number;
|
|
11
|
+
export function getEnvInt(name: string, fallback?: number): number | undefined;
|
|
12
|
+
export function getEnvInt(name: string, fallback?: number): number | undefined {
|
|
13
|
+
const raw = getEnvString(name);
|
|
14
|
+
if (raw === undefined) return fallback;
|
|
15
|
+
const value = Number.parseInt(raw, 10);
|
|
16
|
+
return Number.isNaN(value) ? fallback : value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
export function getEnvDouble(name: string, fallback: number): number;
|
|
21
|
+
export function getEnvDouble(name: string, fallback?: number): number | undefined;
|
|
22
|
+
export function getEnvDouble(name: string, fallback?: number): number | undefined {
|
|
23
|
+
const raw = getEnvString(name);
|
|
24
|
+
if (raw === undefined) return fallback;
|
|
25
|
+
const value = Number.parseFloat(raw);
|
|
26
|
+
return Number.isNaN(value) ? fallback : value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const TRUE_VALUES = new Set(['1', 'true', 'yes', 'on']);
|
|
30
|
+
const FALSE_VALUES = new Set(['0', 'false', 'no', 'off']);
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
export function getEnvBool(name: string, fallback: boolean): boolean;
|
|
34
|
+
export function getEnvBool(name: string, fallback?: boolean): boolean | undefined;
|
|
35
|
+
export function getEnvBool(name: string, fallback?: boolean): boolean | undefined {
|
|
36
|
+
const raw = getEnvString(name);
|
|
37
|
+
if (raw === undefined) return fallback;
|
|
38
|
+
|
|
39
|
+
const normalized = raw.trim().toLowerCase();
|
|
40
|
+
if (TRUE_VALUES.has(normalized)) return true;
|
|
41
|
+
if (FALSE_VALUES.has(normalized)) return false;
|
|
42
|
+
return fallback;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getEnvArray(name: string, fallback: string[], separator?: string): string[];
|
|
46
|
+
export function getEnvArray(name: string, fallback?: string[], separator?: string): string[] | undefined;
|
|
47
|
+
export function getEnvArray(name: string, fallback?: string[], separator: string = ','): string[] | undefined {
|
|
48
|
+
const raw = getEnvString(name);
|
|
49
|
+
if (raw === undefined) return fallback;
|
|
50
|
+
|
|
51
|
+
const values = raw
|
|
52
|
+
.split(separator)
|
|
53
|
+
.map((entry) => entry.trim())
|
|
54
|
+
.filter((entry) => entry.length > 0);
|
|
55
|
+
|
|
56
|
+
return values.length > 0 ? values : fallback;
|
|
57
|
+
}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
|
|
3
|
+
import { FileSystemManager } from './FileSystemManager.js';
|
|
4
|
+
import { AIVendorManager, AIMessage } from './AIVendorManager.js';
|
|
5
|
+
import { Logger } from '../core/Logger.js';
|
|
6
|
+
import { TyrError } from '../core/TyrError.js';
|
|
7
|
+
|
|
8
|
+
import { getEnvInt } from '../core/util/getenv.js';
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
const CONTEXT_FILENAMES = [
|
|
12
|
+
'CLAUDE.md',
|
|
13
|
+
'AGENTS.md',
|
|
14
|
+
'CONTEXT.md',
|
|
15
|
+
'.cursorrules',
|
|
16
|
+
path.join('.github', 'copilot-instructions.md'),
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const GENERATED_FILENAME = 'CLAUDE.md';
|
|
20
|
+
|
|
21
|
+
const SNAPSHOT_TREE_MAX_DEPTH = getEnvInt('SNAPSHOT_TREE_MAX_DEPTH', 5);
|
|
22
|
+
const SNAPSHOT_TREE_MAX_ENTRIES_PER_DIR = getEnvInt('SNAPSHOT_TREE_MAX_ENTRIES_PER_DIR', 250);
|
|
23
|
+
const SNAPSHOT_SECTION_MAX_CHARS = getEnvInt('SNAPSHOT_SECTION_MAX_CHARS', 40000);
|
|
24
|
+
|
|
25
|
+
const DEFAULT_IGNORED_DIRS = new Set<string>([
|
|
26
|
+
'node_modules', '.git', '.hg', '.svn',
|
|
27
|
+
'.turbo', '.next', '.nuxt', '.cache', '.parcel-cache',
|
|
28
|
+
'.vscode', '.idea',
|
|
29
|
+
'dist', 'build', 'out', 'coverage', '.nyc_output',
|
|
30
|
+
'target', 'vendor',
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
const PACKAGE_JSON_FIELDS = ['name', 'version', 'type', 'dependencies', 'devDependencies', 'scripts'] as const;
|
|
34
|
+
|
|
35
|
+
const GUIDELINES_SYSTEM_PROMPT =
|
|
36
|
+
'You are a senior software architect. Given a snapshot of a project (package manifest, README ' +
|
|
37
|
+
'and file structure), produce a concise, well-structured Markdown file of coding guidelines and ' +
|
|
38
|
+
'project conventions (stack, architecture, naming, testing, error handling) meant to be read by ' +
|
|
39
|
+
'an AI coding assistant before making changes. Be specific to this project and avoid generic ' +
|
|
40
|
+
'advice. Output only the Markdown content, with no surrounding commentary.';
|
|
41
|
+
|
|
42
|
+
interface GuidelinesBlock {
|
|
43
|
+
fileName: string;
|
|
44
|
+
content: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @class AIContextManager
|
|
49
|
+
* @description Encuentra, valida y lee los archivos de directrices del proyecto (CLAUDE.md,
|
|
50
|
+
* AGENTS.md, etc.). Si no existen, escanea el proyecto (100% Node.js nativo, sin subprocesos de
|
|
51
|
+
* shell) y le pide al vendor de IA configurado que genere un archivo de directrices óptimo.
|
|
52
|
+
* Expone tanto el texto plano de las directrices (para sesiones de chat que gestionan su propia
|
|
53
|
+
* ventana de contexto) como un mensaje de sistema listo para usar (para completions puntuales).
|
|
54
|
+
*/
|
|
55
|
+
export class AIContextManager {
|
|
56
|
+
private fs: FileSystemManager;
|
|
57
|
+
private ai: AIVendorManager;
|
|
58
|
+
private logger: Logger;
|
|
59
|
+
|
|
60
|
+
constructor(fs: FileSystemManager, ai: AIVendorManager, logger: Logger) {
|
|
61
|
+
this.fs = fs;
|
|
62
|
+
this.ai = ai;
|
|
63
|
+
this.logger = logger;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @method findContextFiles
|
|
68
|
+
* @description Busca los archivos de directrices conocidos directamente dentro del directorio
|
|
69
|
+
* del proyecto.
|
|
70
|
+
* @param {string} dir - Ruta absoluta a la raíz del proyecto.
|
|
71
|
+
* @returns {string[]} Rutas absolutas de los archivos de directrices que existen.
|
|
72
|
+
*/
|
|
73
|
+
public findContextFiles(dir: string): string[] {
|
|
74
|
+
return CONTEXT_FILENAMES
|
|
75
|
+
.map(name => path.join(dir, name))
|
|
76
|
+
.filter(fullPath => this.fs.exists(fullPath));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private async readAndValidate(filePath: string): Promise<string | null> {
|
|
80
|
+
const content = await this.fs.read(filePath);
|
|
81
|
+
if (!content || !content.trim()) return null;
|
|
82
|
+
return content.trim();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @method buildPackageJsonSummary
|
|
87
|
+
* @description Parsea package.json de forma segura (try/catch) y vuelve a serializar
|
|
88
|
+
* únicamente los campos relevantes para el contexto arquitectónico. A diferencia de un
|
|
89
|
+
* `.slice()` a ciegas, nunca entrega al vendor de IA un JSON truncado y sintácticamente roto.
|
|
90
|
+
* @param {string} dir - Ruta absoluta a la raíz del proyecto.
|
|
91
|
+
* @returns {Promise<string | null>} Sección Markdown lista para el snapshot, o null si no
|
|
92
|
+
* hay package.json legible o válido.
|
|
93
|
+
*/
|
|
94
|
+
private async buildPackageJsonSummary(dir: string): Promise<string | null> {
|
|
95
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
96
|
+
if (!this.fs.exists(pkgPath)) return null;
|
|
97
|
+
|
|
98
|
+
const raw = await this.fs.read(pkgPath);
|
|
99
|
+
if (!raw) return null;
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
|
103
|
+
const summary: Record<string, unknown> = {};
|
|
104
|
+
for (const field of PACKAGE_JSON_FIELDS) {
|
|
105
|
+
if (parsed[field] !== undefined) summary[field] = parsed[field];
|
|
106
|
+
}
|
|
107
|
+
return `# package.json (summary)\n\`\`\`json\n${JSON.stringify(summary, null, 2)}\n\`\`\``;
|
|
108
|
+
} catch {
|
|
109
|
+
this.logger.info(`package.json en ${pkgPath} no es JSON válido, se omite del snapshot.`);
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @method scanDirectoryTree
|
|
116
|
+
* @description Escaneo recursivo de directorios agnóstico al SO usando la API nativa de
|
|
117
|
+
* Node.js (sin subprocesos de shell, por lo que funciona igual en Windows, macOS y Linux).
|
|
118
|
+
* Genera un árbol de texto indentado, ignora directorios de ruido y limita las entradas por
|
|
119
|
+
* carpeta para que directorios con cientos de archivos generados no colapsen el snapshot.
|
|
120
|
+
* @param {string} rootDir - Ruta absoluta desde la que empezar a escanear.
|
|
121
|
+
* @param {number} maxDepth - Profundidad máxima de recursión.
|
|
122
|
+
* @param {number} maxEntriesPerDir - Máximo de entradas listadas por carpeta antes de truncar.
|
|
123
|
+
* @param {Set<string>} ignoredDirs - Nombres de directorios a omitir por completo.
|
|
124
|
+
* @returns {Promise<string>} Representación en texto indentado del árbol.
|
|
125
|
+
*/
|
|
126
|
+
private async scanDirectoryTree(
|
|
127
|
+
rootDir: string,
|
|
128
|
+
maxDepth: number = SNAPSHOT_TREE_MAX_DEPTH,
|
|
129
|
+
maxEntriesPerDir: number = SNAPSHOT_TREE_MAX_ENTRIES_PER_DIR,
|
|
130
|
+
ignoredDirs: Set<string> = DEFAULT_IGNORED_DIRS
|
|
131
|
+
): Promise<string> {
|
|
132
|
+
const lines: string[] = [path.basename(rootDir) || rootDir];
|
|
133
|
+
|
|
134
|
+
const walk = async (currentDir: string, depth: number, prefix: string): Promise<void> => {
|
|
135
|
+
if (depth > maxDepth) return;
|
|
136
|
+
|
|
137
|
+
let entries;
|
|
138
|
+
try {
|
|
139
|
+
entries = await this.fs.readdir(currentDir, { withFileTypes: true });
|
|
140
|
+
} catch {
|
|
141
|
+
// Errores de permisos o carpetas borradas a mitad de escaneo no deben abortar
|
|
142
|
+
// todo el snapshot, simplemente se omiten.
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
entries = entries
|
|
147
|
+
.filter(entry => !(entry.isDirectory() && ignoredDirs.has(entry.name)))
|
|
148
|
+
.sort((a, b) => {
|
|
149
|
+
if (a.isDirectory() !== b.isDirectory()) return a.isDirectory() ? -1 : 1;
|
|
150
|
+
return a.name.localeCompare(b.name);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const visible = entries.slice(0, maxEntriesPerDir);
|
|
154
|
+
const hiddenCount = entries.length - visible.length;
|
|
155
|
+
|
|
156
|
+
for (const entry of visible) {
|
|
157
|
+
lines.push(`${prefix}${entry.name}${entry.isDirectory() ? '/' : ''}`);
|
|
158
|
+
if (entry.isDirectory()) {
|
|
159
|
+
await walk(path.join(currentDir, entry.name), depth + 1, `${prefix} `);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (hiddenCount > 0) {
|
|
164
|
+
lines.push(`${prefix}… y ${hiddenCount} elemento(s) más`);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
await walk(rootDir, 1, ' ');
|
|
169
|
+
return lines.join('\n');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* @method buildProjectSnapshot
|
|
174
|
+
* @description Ensambla el material que necesita el vendor de IA para escribir directrices:
|
|
175
|
+
* un resumen seguro de package.json, el README y un árbol de directorios nativo. Sin
|
|
176
|
+
* subprocesos de shell.
|
|
177
|
+
*/
|
|
178
|
+
private async buildProjectSnapshot(dir: string): Promise<string> {
|
|
179
|
+
const parts: string[] = [];
|
|
180
|
+
|
|
181
|
+
const pkgSummary = await this.buildPackageJsonSummary(dir);
|
|
182
|
+
if (pkgSummary) parts.push(pkgSummary);
|
|
183
|
+
|
|
184
|
+
const readmePath = path.join(dir, 'README.md');
|
|
185
|
+
if (this.fs.exists(readmePath)) {
|
|
186
|
+
const readme = await this.fs.read(readmePath);
|
|
187
|
+
// El README es texto libre, no estructurado: aquí el truncado por longitud sí es seguro.
|
|
188
|
+
if (readme) parts.push(`# README.md\n${readme.slice(0, SNAPSHOT_SECTION_MAX_CHARS)}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const tree = await this.scanDirectoryTree(dir).catch(() => '');
|
|
192
|
+
if (tree) parts.push(`# Project structure\n${tree.slice(0, SNAPSHOT_SECTION_MAX_CHARS)}`);
|
|
193
|
+
|
|
194
|
+
return parts.join('\n\n');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* @method generateContextFile
|
|
199
|
+
* @description Escanea el proyecto y le pide al vendor de IA configurado que sintetice un
|
|
200
|
+
* archivo de directrices, luego lo escribe como CLAUDE.md en la raíz del proyecto.
|
|
201
|
+
* @param {string} dir - Ruta absoluta a la raíz del proyecto.
|
|
202
|
+
* @returns {Promise<string>} Ruta absoluta del archivo generado.
|
|
203
|
+
*/
|
|
204
|
+
public async generateContextFile(dir: string): Promise<string> {
|
|
205
|
+
this.logger.info('No se encontró archivo de contexto. Analizando el proyecto para generar uno...');
|
|
206
|
+
|
|
207
|
+
const snapshot = await this.buildProjectSnapshot(dir);
|
|
208
|
+
const messages: AIMessage[] = [
|
|
209
|
+
{ role: 'system', content: GUIDELINES_SYSTEM_PROMPT },
|
|
210
|
+
{ role: 'user', content: snapshot || 'The project has no readable package.json, README or file tree.' },
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
let result;
|
|
214
|
+
try {
|
|
215
|
+
result = await this.ai.complete(messages, { temperature: 0.2 });
|
|
216
|
+
} catch (err) {
|
|
217
|
+
throw new TyrError(
|
|
218
|
+
`No se pudo generar el archivo de contexto para ${dir}`,
|
|
219
|
+
err,
|
|
220
|
+
'Revisa la configuración de tu vendor de IA y la conectividad de red, luego reintenta.'
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const targetPath = path.join(dir, GENERATED_FILENAME);
|
|
225
|
+
await this.fs.write(targetPath, result.content.trim() + '\n');
|
|
226
|
+
|
|
227
|
+
this.logger.success(`Archivo de contexto generado en: ${targetPath}`);
|
|
228
|
+
return targetPath;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* @method readGuidelines
|
|
233
|
+
* @description Encuentra los archivos de directrices existentes (generando uno vía IA si no
|
|
234
|
+
* hay ninguno), los lee y valida, y los devuelve como bloques discretos etiquetados — sin
|
|
235
|
+
* colapsarlos en un único string de mensaje de sistema. Esta es la pieza clave para sesiones
|
|
236
|
+
* de chat de larga duración: quien necesite gestionar una ventana de contexto continua (p. ej.
|
|
237
|
+
* un AIChatSessionManager) puede inspeccionar, cachear o descartar bloques individuales en
|
|
238
|
+
* lugar de releer disco y reinyectar un system prompt duplicado en cada turno.
|
|
239
|
+
* @param {string} dir - Ruta absoluta a la raíz del proyecto.
|
|
240
|
+
* @returns {Promise<GuidelinesBlock[]>}
|
|
241
|
+
*/
|
|
242
|
+
public async readGuidelines(dir: string): Promise<GuidelinesBlock[]> {
|
|
243
|
+
let files = this.findContextFiles(dir);
|
|
244
|
+
|
|
245
|
+
if (files.length === 0) {
|
|
246
|
+
const generated = await this.generateContextFile(dir);
|
|
247
|
+
files = [generated];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const blocks: GuidelinesBlock[] = [];
|
|
251
|
+
for (const file of files) {
|
|
252
|
+
const content = await this.readAndValidate(file);
|
|
253
|
+
if (content) blocks.push({ fileName: path.basename(file), content });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (blocks.length === 0) {
|
|
257
|
+
throw new TyrError(
|
|
258
|
+
`Se encontraron archivos de contexto pero están vacíos o son ilegibles en: ${dir}`,
|
|
259
|
+
null,
|
|
260
|
+
'Revisa el contenido de tu archivo CLAUDE.md/AGENTS.md.'
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return blocks;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* @method getGuidelinesText
|
|
269
|
+
* @description Envoltorio de conveniencia sobre readGuidelines() que devuelve el texto plano
|
|
270
|
+
* combinado de las directrices, sin envolver en roles/mensajes. Pensado para un
|
|
271
|
+
* AIChatSessionManager: se obtiene una vez por sesión, se cachea, y se gestiona dentro de la
|
|
272
|
+
* ventana de contexto de la conversación en lugar de llamar a getContext() (con su relectura
|
|
273
|
+
* de disco y reenvío de un system message completo) en cada turno.
|
|
274
|
+
* @param {string} dir - Ruta absoluta a la raíz del proyecto.
|
|
275
|
+
* @returns {Promise<string>}
|
|
276
|
+
*/
|
|
277
|
+
public async getGuidelinesText(dir: string): Promise<string> {
|
|
278
|
+
const blocks = await this.readGuidelines(dir);
|
|
279
|
+
return blocks.map(b => `<!-- ${b.fileName} -->\n${b.content}`).join('\n\n');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* @method getContext
|
|
284
|
+
* @description Punto de entrada para uso puntual: encuentra/genera archivos de directrices y
|
|
285
|
+
* los empaqueta como un único mensaje 'system' listo para prepender a un prompt. Para sesiones
|
|
286
|
+
* de chat continuas, usa getGuidelinesText() junto con tu propia gestión de ventana de contexto
|
|
287
|
+
* en lugar de llamar a este método en cada turno.
|
|
288
|
+
* @param {string} dir - Ruta absoluta a la raíz del proyecto.
|
|
289
|
+
* @returns {Promise<AIMessage[]>}
|
|
290
|
+
* @example
|
|
291
|
+
* const contextMessages = await context.getContext(process.cwd());
|
|
292
|
+
* const result = await ai.complete([...contextMessages, { role: 'user', content: 'Fix this bug...' }]);
|
|
293
|
+
*/
|
|
294
|
+
public async getContext(dir: string): Promise<AIMessage[]> {
|
|
295
|
+
const text = await this.getGuidelinesText(dir);
|
|
296
|
+
return [{ role: 'system', content: text }];
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export const AIContextManagerTests = {
|
|
301
|
+
findContextFiles: { dir: '~/Projects/TyrFramework' },
|
|
302
|
+
};
|