@skroyc/librarian 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/CHANGELOG.md +176 -0
- package/LICENSE +210 -0
- package/README.md +614 -0
- package/biome.jsonc +9 -0
- package/dist/agents/context-schema.d.ts +17 -0
- package/dist/agents/context-schema.d.ts.map +1 -0
- package/dist/agents/context-schema.js +16 -0
- package/dist/agents/context-schema.js.map +1 -0
- package/dist/agents/react-agent.d.ts +38 -0
- package/dist/agents/react-agent.d.ts.map +1 -0
- package/dist/agents/react-agent.js +719 -0
- package/dist/agents/react-agent.js.map +1 -0
- package/dist/agents/tool-runtime.d.ts +7 -0
- package/dist/agents/tool-runtime.d.ts.map +1 -0
- package/dist/agents/tool-runtime.js +2 -0
- package/dist/agents/tool-runtime.js.map +1 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +172 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +243 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +470 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/file-finding.tool.d.ts +24 -0
- package/dist/tools/file-finding.tool.d.ts.map +1 -0
- package/dist/tools/file-finding.tool.js +198 -0
- package/dist/tools/file-finding.tool.js.map +1 -0
- package/dist/tools/file-listing.tool.d.ts +12 -0
- package/dist/tools/file-listing.tool.d.ts.map +1 -0
- package/dist/tools/file-listing.tool.js +132 -0
- package/dist/tools/file-listing.tool.js.map +1 -0
- package/dist/tools/file-reading.tool.d.ts +9 -0
- package/dist/tools/file-reading.tool.d.ts.map +1 -0
- package/dist/tools/file-reading.tool.js +112 -0
- package/dist/tools/file-reading.tool.js.map +1 -0
- package/dist/tools/grep-content.tool.d.ts +27 -0
- package/dist/tools/grep-content.tool.d.ts.map +1 -0
- package/dist/tools/grep-content.tool.js +229 -0
- package/dist/tools/grep-content.tool.js.map +1 -0
- package/dist/utils/file-utils.d.ts +2 -0
- package/dist/utils/file-utils.d.ts.map +1 -0
- package/dist/utils/file-utils.js +28 -0
- package/dist/utils/file-utils.js.map +1 -0
- package/dist/utils/logger.d.ts +32 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +177 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/path-utils.d.ts +2 -0
- package/dist/utils/path-utils.d.ts.map +1 -0
- package/dist/utils/path-utils.js +9 -0
- package/dist/utils/path-utils.js.map +1 -0
- package/package.json +84 -0
- package/src/agents/context-schema.ts +61 -0
- package/src/agents/react-agent.ts +928 -0
- package/src/agents/tool-runtime.ts +21 -0
- package/src/cli.ts +206 -0
- package/src/config.ts +309 -0
- package/src/index.ts +628 -0
- package/src/tools/file-finding.tool.ts +324 -0
- package/src/tools/file-listing.tool.ts +212 -0
- package/src/tools/file-reading.tool.ts +154 -0
- package/src/tools/grep-content.tool.ts +325 -0
- package/src/utils/file-utils.ts +39 -0
- package/src/utils/logger.ts +295 -0
- package/src/utils/path-utils.ts +17 -0
- package/tsconfig.json +37 -0
- package/tsconfig.test.json +17 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Librarian CLI - Technology Research Agent
|
|
3
|
+
* Main entry point for the application
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { clone, fetch, checkout } from 'isomorphic-git';
|
|
7
|
+
import http from 'isomorphic-git/http/node';
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
|
|
11
|
+
import { ReactAgent } from './agents/react-agent.js';
|
|
12
|
+
import type { AgentContext } from './agents/context-schema.js';
|
|
13
|
+
import { logger } from './utils/logger.js';
|
|
14
|
+
import os from 'node:os';
|
|
15
|
+
|
|
16
|
+
export interface LibrarianConfig {
|
|
17
|
+
technologies: {
|
|
18
|
+
[group: string]: {
|
|
19
|
+
[tech: string]: {
|
|
20
|
+
repo: string;
|
|
21
|
+
branch?: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
aiProvider: {
|
|
27
|
+
type: 'openai' | 'anthropic' | 'google' | 'openai-compatible' | 'anthropic-compatible' | 'claude-code' | 'gemini-cli';
|
|
28
|
+
apiKey: string;
|
|
29
|
+
model?: string;
|
|
30
|
+
baseURL?: string;
|
|
31
|
+
};
|
|
32
|
+
workingDir: string;
|
|
33
|
+
repos_path?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class Librarian {
|
|
37
|
+
private readonly config: LibrarianConfig;
|
|
38
|
+
|
|
39
|
+
constructor(config: LibrarianConfig) {
|
|
40
|
+
// Validate AI provider type
|
|
41
|
+
const validProviderTypes = [
|
|
42
|
+
'openai',
|
|
43
|
+
'anthropic',
|
|
44
|
+
'google',
|
|
45
|
+
'openai-compatible',
|
|
46
|
+
'anthropic-compatible',
|
|
47
|
+
'claude-code',
|
|
48
|
+
'gemini-cli',
|
|
49
|
+
] as const;
|
|
50
|
+
type ValidProviderType = typeof validProviderTypes[number];
|
|
51
|
+
|
|
52
|
+
if (!validProviderTypes.includes(config.aiProvider.type as ValidProviderType)) {
|
|
53
|
+
throw new Error(`Unsupported AI provider type: ${config.aiProvider.type}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
this.config = config;
|
|
57
|
+
|
|
58
|
+
logger.info('LIBRARIAN', 'Initializing librarian', {
|
|
59
|
+
aiProviderType: config.aiProvider.type,
|
|
60
|
+
model: config.aiProvider.model,
|
|
61
|
+
workingDir: config.workingDir.replace(os.homedir(), '~'),
|
|
62
|
+
reposPath: config.repos_path ? config.repos_path.replace(os.homedir(), '~') : 'workingDir'
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async initialize(): Promise<void> {
|
|
67
|
+
// Check if Claude CLI is available if using claude-code provider
|
|
68
|
+
if (this.config.aiProvider.type === 'claude-code') {
|
|
69
|
+
try {
|
|
70
|
+
const { execSync } = await import('node:child_process');
|
|
71
|
+
execSync('claude --version', { stdio: 'ignore' });
|
|
72
|
+
logger.info('LIBRARIAN', 'Claude CLI verified');
|
|
73
|
+
} catch {
|
|
74
|
+
logger.error('LIBRARIAN', 'Claude CLI not found in PATH', undefined, { type: 'claude-code' });
|
|
75
|
+
console.error('Error: "claude" CLI not found. Please install it to use the "claude-code" provider.');
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Check if Gemini CLI is available if using gemini-cli provider
|
|
81
|
+
if (this.config.aiProvider.type === 'gemini-cli') {
|
|
82
|
+
try {
|
|
83
|
+
const { execSync } = await import('node:child_process');
|
|
84
|
+
execSync('gemini --version', { stdio: 'ignore' });
|
|
85
|
+
logger.info('LIBRARIAN', 'Gemini CLI verified');
|
|
86
|
+
} catch {
|
|
87
|
+
logger.error('LIBRARIAN', 'Gemini CLI not found in PATH', undefined, { type: 'gemini-cli' });
|
|
88
|
+
console.error('Error: "gemini" CLI not found. Please install it to use the "gemini-cli" provider.');
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Create working directory if it doesn't exist
|
|
94
|
+
const workDir = this.config.repos_path || this.config.workingDir;
|
|
95
|
+
if (fs.existsSync(workDir)) {
|
|
96
|
+
logger.debug('LIBRARIAN', 'Working directory already exists', { path: workDir.replace(os.homedir(), '~') });
|
|
97
|
+
} else {
|
|
98
|
+
logger.info('LIBRARIAN', 'Creating working directory', { path: workDir.replace(os.homedir(), '~') });
|
|
99
|
+
fs.mkdirSync(workDir, { recursive: true });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
logger.info('LIBRARIAN', 'Initialization complete');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
resolveTechnology(qualifiedName: string): { name: string, group: string, repo: string, branch: string } | undefined {
|
|
106
|
+
logger.debug('LIBRARIAN', 'Resolving technology', { qualifiedName });
|
|
107
|
+
|
|
108
|
+
let group: string | undefined;
|
|
109
|
+
let name: string;
|
|
110
|
+
|
|
111
|
+
if (qualifiedName.includes(':')) {
|
|
112
|
+
const parts = qualifiedName.split(':');
|
|
113
|
+
group = parts[0];
|
|
114
|
+
name = parts[1] || '';
|
|
115
|
+
} else {
|
|
116
|
+
name = qualifiedName;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (group) {
|
|
120
|
+
const groupTechs = this.config.technologies[group];
|
|
121
|
+
const tech = groupTechs ? groupTechs[name] : undefined;
|
|
122
|
+
if (tech) {
|
|
123
|
+
const result = { name, group, repo: tech.repo, branch: tech.branch || 'main' };
|
|
124
|
+
logger.debug('LIBRARIAN', 'Technology resolved with explicit group', { name, group, repoHost: tech.repo.split('/')[2] || 'unknown' });
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
for (const [groupName, techs] of Object.entries(this.config.technologies)) {
|
|
129
|
+
const tech = techs[name];
|
|
130
|
+
if (tech) {
|
|
131
|
+
const result = { name, group: groupName, repo: tech.repo, branch: tech.branch || 'main' };
|
|
132
|
+
logger.debug('LIBRARIAN', 'Technology resolved by search', { name, group: groupName, repoHost: tech.repo.split('/')[2] || 'unknown' });
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
logger.debug('LIBRARIAN', 'Technology not found in configuration', { qualifiedName });
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private getSecureGroupPath(groupName: string): string {
|
|
142
|
+
logger.debug('PATH', 'Validating group path', { groupName });
|
|
143
|
+
|
|
144
|
+
if (groupName.includes('../') || groupName.includes('..\\') || groupName.startsWith('..')) {
|
|
145
|
+
logger.error('PATH', 'Group name contains path traversal characters', undefined, { groupName });
|
|
146
|
+
throw new Error(`Group name "${groupName}" contains invalid path characters`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const sanitizedGroupName = path.basename(groupName);
|
|
150
|
+
const workDir = this.config.repos_path || this.config.workingDir;
|
|
151
|
+
const groupPath = path.join(workDir, sanitizedGroupName);
|
|
152
|
+
|
|
153
|
+
const resolvedWorkingDir = path.resolve(workDir);
|
|
154
|
+
const resolvedGroupPath = path.resolve(groupPath);
|
|
155
|
+
|
|
156
|
+
if (!resolvedGroupPath.startsWith(resolvedWorkingDir)) {
|
|
157
|
+
logger.error('PATH', 'Group path escapes working directory sandbox', undefined, { groupName });
|
|
158
|
+
throw new Error(`Group name "${groupName}" attempts to escape the working directory sandbox`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
logger.debug('PATH', 'Group path validated', { groupName, path: groupPath.replace(os.homedir(), '~') });
|
|
162
|
+
return groupPath;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private getSecureRepoPath(repoName: string, groupName = 'default'): string {
|
|
166
|
+
logger.debug('PATH', 'Validating repo path', { repoName, groupName });
|
|
167
|
+
|
|
168
|
+
// Check for path traversal attempts in the repoName before sanitizing
|
|
169
|
+
if (repoName.includes('../') || repoName.includes('..\\') || repoName.startsWith('..')) {
|
|
170
|
+
logger.error('PATH', 'Repo name contains path traversal characters', undefined, { repoName, groupName });
|
|
171
|
+
throw new Error(`Repository name "${repoName}" contains invalid path characters`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Sanitize the names
|
|
175
|
+
const sanitizedRepoName = path.basename(repoName);
|
|
176
|
+
const groupPath = this.getSecureGroupPath(groupName);
|
|
177
|
+
const repoPath = path.join(groupPath, sanitizedRepoName);
|
|
178
|
+
|
|
179
|
+
// Verify that the resulting path is within the group directory (which is already sandboxed)
|
|
180
|
+
const resolvedGroupDir = path.resolve(groupPath);
|
|
181
|
+
const resolvedRepoPath = path.resolve(repoPath);
|
|
182
|
+
|
|
183
|
+
if (!resolvedRepoPath.startsWith(resolvedGroupDir)) {
|
|
184
|
+
logger.error('PATH', 'Repo path escapes group sandbox', undefined, { repoName, groupName });
|
|
185
|
+
throw new Error(`Repository name "${repoName}" attempts to escape the group sandbox`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
logger.debug('PATH', 'Repo path validated', { repoName, groupName, path: repoPath.replace(os.homedir(), '~') });
|
|
189
|
+
return repoPath;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async updateRepository(repoName: string, groupName = 'default'): Promise<void> {
|
|
193
|
+
logger.info('GIT', 'Updating repository', { repoName, groupName });
|
|
194
|
+
|
|
195
|
+
const timingId = logger.timingStart('updateRepository');
|
|
196
|
+
|
|
197
|
+
const repoPath = this.getSecureRepoPath(repoName, groupName);
|
|
198
|
+
const gitPath = path.join(repoPath, '.git');
|
|
199
|
+
|
|
200
|
+
if (!fs.existsSync(repoPath)) {
|
|
201
|
+
logger.error('GIT', 'Repository path does not exist', undefined, { repoName, repoPath: repoPath.replace(os.homedir(), '~') });
|
|
202
|
+
throw new Error(`Repository ${repoName} does not exist at ${repoPath}. Cannot update.`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!fs.existsSync(gitPath)) {
|
|
206
|
+
logger.error('GIT', 'Directory is not a git repository', undefined, { repoName, repoPath: repoPath.replace(os.homedir(), '~') });
|
|
207
|
+
throw new Error(`Directory ${repoName} exists at ${repoPath} but is not a git repository. Cannot update.`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Fetch updates from the remote
|
|
211
|
+
logger.debug('GIT', 'Fetching updates from remote');
|
|
212
|
+
await fetch({
|
|
213
|
+
fs,
|
|
214
|
+
http,
|
|
215
|
+
dir: repoPath,
|
|
216
|
+
singleBranch: true,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const tech = this.resolveTechnology(`${groupName}:${repoName}`) || this.resolveTechnology(repoName);
|
|
220
|
+
const branch = tech?.branch || 'main';
|
|
221
|
+
|
|
222
|
+
// Checkout the latest version
|
|
223
|
+
logger.debug('GIT', 'Checking out branch', { branch });
|
|
224
|
+
await checkout({
|
|
225
|
+
fs,
|
|
226
|
+
dir: repoPath,
|
|
227
|
+
ref: `origin/${branch}`,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
logger.timingEnd(timingId, 'GIT', `Repository updated: ${repoName}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async syncRepository(repoName: string): Promise<string> {
|
|
234
|
+
logger.info('GIT', 'Syncing repository', { repoName });
|
|
235
|
+
|
|
236
|
+
const tech = this.resolveTechnology(repoName);
|
|
237
|
+
|
|
238
|
+
if (!tech) {
|
|
239
|
+
logger.error('GIT', 'Technology not found in configuration', undefined, { repoName });
|
|
240
|
+
throw new Error(`Repository ${repoName} not found in configuration`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const repoPath = this.getSecureRepoPath(tech.name, tech.group);
|
|
244
|
+
|
|
245
|
+
// Check if this is a local path (not a remote URL)
|
|
246
|
+
const isLocalRepo = !(tech.repo.startsWith('http://') || tech.repo.startsWith('https://'));
|
|
247
|
+
|
|
248
|
+
if (fs.existsSync(repoPath)) {
|
|
249
|
+
// Repository exists
|
|
250
|
+
if (isLocalRepo) {
|
|
251
|
+
// For local repos, skip git operations - just use existing files
|
|
252
|
+
logger.debug('GIT', 'Local repository exists, skipping git operations');
|
|
253
|
+
return repoPath;
|
|
254
|
+
}
|
|
255
|
+
// Remote repository, update it (no-op for local repos to avoid isomorphic-git issues with local paths)
|
|
256
|
+
if (!isLocalRepo) {
|
|
257
|
+
logger.debug('GIT', 'Repository exists, performing update');
|
|
258
|
+
await this.updateRepository(tech.name, tech.group);
|
|
259
|
+
}
|
|
260
|
+
} else {
|
|
261
|
+
// Repository doesn't exist
|
|
262
|
+
if (isLocalRepo) {
|
|
263
|
+
// Local repo doesn't exist - this is an error in test setup
|
|
264
|
+
logger.error('GIT', 'Local repository path does not exist', undefined, { repoName, repoPath });
|
|
265
|
+
throw new Error(`Local repository ${repoName} does not exist at ${repoPath}`);
|
|
266
|
+
}
|
|
267
|
+
// Remote repository, clone it
|
|
268
|
+
logger.debug('GIT', 'Repository does not exist, performing clone');
|
|
269
|
+
return await this.cloneRepository(tech.name, tech.repo, tech.group, tech.branch);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return repoPath;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async cloneRepository(repoName: string, repoUrl: string, groupName = 'default', branch = 'main'): Promise<string> {
|
|
276
|
+
logger.info('GIT', 'Cloning repository', { repoName, repoHost: repoUrl.split('/')[2] || 'unknown', branch, groupName });
|
|
277
|
+
|
|
278
|
+
const timingId = logger.timingStart('cloneRepository');
|
|
279
|
+
|
|
280
|
+
const repoPath = this.getSecureRepoPath(repoName, groupName);
|
|
281
|
+
|
|
282
|
+
// Check if repository already exists
|
|
283
|
+
if (fs.existsSync(repoPath)) {
|
|
284
|
+
// Check if it's a git repository by checking for .git folder
|
|
285
|
+
const gitPath = path.join(repoPath, '.git');
|
|
286
|
+
if (fs.existsSync(gitPath)) {
|
|
287
|
+
logger.debug('GIT', 'Repository already exists as git repo, updating instead');
|
|
288
|
+
await this.updateRepository(repoName, groupName);
|
|
289
|
+
return repoPath;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Ensure group directory exists
|
|
294
|
+
const groupPath = this.getSecureGroupPath(groupName);
|
|
295
|
+
if (!fs.existsSync(groupPath)) {
|
|
296
|
+
logger.debug('GIT', 'Creating group directory', { groupName, path: groupPath.replace(os.homedir(), '~') });
|
|
297
|
+
fs.mkdirSync(groupPath, { recursive: true });
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
logger.debug('GIT', 'Starting shallow clone', { depth: 1 });
|
|
301
|
+
await clone({
|
|
302
|
+
fs,
|
|
303
|
+
http,
|
|
304
|
+
dir: repoPath,
|
|
305
|
+
url: repoUrl,
|
|
306
|
+
ref: branch,
|
|
307
|
+
singleBranch: true,
|
|
308
|
+
depth: 1, // Shallow clone for faster operation
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// Count files cloned
|
|
312
|
+
let fileCount = 0;
|
|
313
|
+
const countFiles = (dir: string) => {
|
|
314
|
+
try {
|
|
315
|
+
const files = fs.readdirSync(dir, { withFileTypes: true });
|
|
316
|
+
for (const file of files) {
|
|
317
|
+
if (file.name === '.git') {
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (file.isDirectory()) {
|
|
321
|
+
countFiles(path.join(dir, file.name));
|
|
322
|
+
} else {
|
|
323
|
+
fileCount++;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
} catch {
|
|
327
|
+
// Ignore errors
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
countFiles(repoPath);
|
|
331
|
+
|
|
332
|
+
logger.debug('GIT', 'Clone completed', { fileCount });
|
|
333
|
+
|
|
334
|
+
logger.timingEnd(timingId, 'GIT', `Repository cloned: ${repoName}`);
|
|
335
|
+
return repoPath;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async queryRepository(repoName: string, query: string): Promise<string> {
|
|
339
|
+
logger.info('LIBRARIAN', 'Querying repository', { repoName, queryLength: query.length });
|
|
340
|
+
|
|
341
|
+
const timingId = logger.timingStart('queryRepository');
|
|
342
|
+
|
|
343
|
+
const tech = this.resolveTechnology(repoName);
|
|
344
|
+
|
|
345
|
+
if (!tech) {
|
|
346
|
+
logger.error('LIBRARIAN', 'Technology not found in configuration', undefined, { repoName });
|
|
347
|
+
throw new Error(`Repository ${repoName} not found in configuration`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Clone or sync the repository first
|
|
351
|
+
const repoPath = await this.syncRepository(repoName);
|
|
352
|
+
|
|
353
|
+
// Construct context object with working directory
|
|
354
|
+
const context: AgentContext = {
|
|
355
|
+
workingDir: repoPath,
|
|
356
|
+
group: tech.group,
|
|
357
|
+
technology: tech.name,
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
logger.debug('LIBRARIAN', 'Initializing agent for query with context', {
|
|
361
|
+
workingDir: context.workingDir.replace(os.homedir(), '~'),
|
|
362
|
+
group: context.group,
|
|
363
|
+
technology: context.technology
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// Initialize the agent
|
|
367
|
+
const agent = new ReactAgent({
|
|
368
|
+
aiProvider: this.config.aiProvider,
|
|
369
|
+
workingDir: repoPath,
|
|
370
|
+
technology: {
|
|
371
|
+
name: tech.name,
|
|
372
|
+
repository: tech.repo,
|
|
373
|
+
branch: tech.branch
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
await agent.initialize();
|
|
377
|
+
|
|
378
|
+
// Execute the query using the agent with context
|
|
379
|
+
const result = await agent.queryRepository(repoPath, query, context);
|
|
380
|
+
|
|
381
|
+
logger.timingEnd(timingId, 'LIBRARIAN', `Query completed: ${repoName}`);
|
|
382
|
+
logger.info('LIBRARIAN', 'Query result received', { repoName, responseLength: result.length });
|
|
383
|
+
|
|
384
|
+
return result;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async *streamRepository(repoName: string, query: string): AsyncGenerator<string, void, unknown> {
|
|
388
|
+
logger.info('LIBRARIAN', 'Streaming repository query', { repoName, queryLength: query.length });
|
|
389
|
+
|
|
390
|
+
const timingId = logger.timingStart('streamRepository');
|
|
391
|
+
|
|
392
|
+
const tech = this.resolveTechnology(repoName);
|
|
393
|
+
|
|
394
|
+
if (!tech) {
|
|
395
|
+
logger.error('LIBRARIAN', 'Technology not found in configuration', undefined, { repoName });
|
|
396
|
+
throw new Error(`Repository ${repoName} not found in configuration`);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Set up interruption handling at Librarian level
|
|
400
|
+
let isInterrupted = false;
|
|
401
|
+
const cleanup = () => {
|
|
402
|
+
isInterrupted = true;
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
// Listen for interruption signals (Ctrl+C)
|
|
406
|
+
logger.debug('LIBRARIAN', 'Setting up interruption handlers');
|
|
407
|
+
process.on('SIGINT', cleanup);
|
|
408
|
+
process.on('SIGTERM', cleanup);
|
|
409
|
+
|
|
410
|
+
try {
|
|
411
|
+
// Clone or sync repository first
|
|
412
|
+
const repoPath = await this.syncRepository(repoName);
|
|
413
|
+
|
|
414
|
+
// Check for interruption after sync
|
|
415
|
+
if (isInterrupted) {
|
|
416
|
+
logger.warn('LIBRARIAN', 'Repository sync interrupted by user', { repoName });
|
|
417
|
+
yield '[Repository sync interrupted by user]';
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Construct context object with working directory
|
|
422
|
+
const context: AgentContext = {
|
|
423
|
+
workingDir: repoPath,
|
|
424
|
+
group: tech.group,
|
|
425
|
+
technology: tech.name,
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
logger.debug('LIBRARIAN', 'Initializing agent for streaming query with context', {
|
|
429
|
+
workingDir: context.workingDir.replace(os.homedir(), '~'),
|
|
430
|
+
group: context.group,
|
|
431
|
+
technology: context.technology
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
// Initialize agent
|
|
435
|
+
const agent = new ReactAgent({
|
|
436
|
+
aiProvider: this.config.aiProvider,
|
|
437
|
+
workingDir: repoPath,
|
|
438
|
+
technology: {
|
|
439
|
+
name: tech.name,
|
|
440
|
+
repository: tech.repo,
|
|
441
|
+
branch: tech.branch
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
await agent.initialize();
|
|
445
|
+
|
|
446
|
+
// Check for interruption after initialization
|
|
447
|
+
if (isInterrupted) {
|
|
448
|
+
logger.warn('LIBRARIAN', 'Agent initialization interrupted by user', { repoName });
|
|
449
|
+
yield '[Agent initialization interrupted by user]';
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Execute streaming query using agent with context
|
|
454
|
+
logger.debug('LIBRARIAN', 'Starting stream from agent');
|
|
455
|
+
yield* agent.streamRepository(repoPath, query, context);
|
|
456
|
+
} catch (error) {
|
|
457
|
+
// Handle repository-level errors
|
|
458
|
+
let errorMessage = 'Unknown error';
|
|
459
|
+
|
|
460
|
+
if (error instanceof Error) {
|
|
461
|
+
if (error.message.includes('not found in configuration')) {
|
|
462
|
+
errorMessage = error.message;
|
|
463
|
+
} else if (error.message.includes('git') || error.message.includes('clone')) {
|
|
464
|
+
errorMessage = `Repository operation failed: ${error.message}`;
|
|
465
|
+
} else if (error.message.includes('timeout')) {
|
|
466
|
+
errorMessage = 'Repository operation timed out';
|
|
467
|
+
} else {
|
|
468
|
+
errorMessage = `Repository error: ${error.message}`;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
logger.error('LIBRARIAN', 'Stream error', error instanceof Error ? error : new Error(errorMessage), { repoName });
|
|
473
|
+
yield `\n[Error: ${errorMessage}]`;
|
|
474
|
+
throw error;
|
|
475
|
+
} finally {
|
|
476
|
+
// Clean up event listeners
|
|
477
|
+
process.removeListener('SIGINT', cleanup);
|
|
478
|
+
process.removeListener('SIGTERM', cleanup);
|
|
479
|
+
logger.timingEnd(timingId, 'LIBRARIAN', `Stream completed: ${repoName}`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async queryGroup(groupName: string, query: string): Promise<string> {
|
|
484
|
+
logger.info('LIBRARIAN', 'Querying group', { groupName, queryLength: query.length });
|
|
485
|
+
|
|
486
|
+
const timingId = logger.timingStart('queryGroup');
|
|
487
|
+
|
|
488
|
+
// Validate group exists
|
|
489
|
+
if (!this.config.technologies[groupName]) {
|
|
490
|
+
logger.error('LIBRARIAN', 'Group not found in configuration', undefined, { groupName });
|
|
491
|
+
throw new Error(`Group ${groupName} not found in configuration`);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Get the group directory path
|
|
495
|
+
const groupPath = this.getSecureGroupPath(groupName);
|
|
496
|
+
|
|
497
|
+
// Sync all technologies in the group first
|
|
498
|
+
const technologies = this.config.technologies[groupName];
|
|
499
|
+
if (technologies) {
|
|
500
|
+
const techNames = Object.keys(technologies);
|
|
501
|
+
logger.info('LIBRARIAN', 'Syncing all technologies in group', { groupName, techCount: techNames.length });
|
|
502
|
+
for (const techName of techNames) {
|
|
503
|
+
await this.syncRepository(techName);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Construct context object for group-level query
|
|
508
|
+
const context: AgentContext = {
|
|
509
|
+
workingDir: groupPath,
|
|
510
|
+
group: groupName,
|
|
511
|
+
technology: '', // No specific technology for group-level queries
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
logger.debug('LIBRARIAN', 'Initializing agent for group query with context', {
|
|
515
|
+
workingDir: context.workingDir.replace(os.homedir(), '~'),
|
|
516
|
+
group: context.group
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
// Initialize the agent with the group directory as working directory
|
|
520
|
+
const agent = new ReactAgent({
|
|
521
|
+
aiProvider: this.config.aiProvider,
|
|
522
|
+
workingDir: groupPath
|
|
523
|
+
});
|
|
524
|
+
await agent.initialize();
|
|
525
|
+
|
|
526
|
+
// Execute the query using the agent with context
|
|
527
|
+
const result = await agent.queryRepository(groupPath, query, context);
|
|
528
|
+
|
|
529
|
+
logger.timingEnd(timingId, 'LIBRARIAN', `Group query completed: ${groupName}`);
|
|
530
|
+
logger.info('LIBRARIAN', 'Group query result received', { groupName, responseLength: result.length });
|
|
531
|
+
|
|
532
|
+
return result;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
async *streamGroup(groupName: string, query: string): AsyncGenerator<string, void, unknown> {
|
|
536
|
+
logger.info('LIBRARIAN', 'Streaming group query', { groupName, queryLength: query.length });
|
|
537
|
+
|
|
538
|
+
const timingId = logger.timingStart('streamGroup');
|
|
539
|
+
|
|
540
|
+
if (!this.config.technologies[groupName]) {
|
|
541
|
+
logger.error('LIBRARIAN', 'Group not found in configuration', undefined, { groupName });
|
|
542
|
+
throw new Error(`Group ${groupName} not found in configuration`);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const groupPath = this.getSecureGroupPath(groupName);
|
|
546
|
+
|
|
547
|
+
let isInterrupted = false;
|
|
548
|
+
const cleanup = () => {
|
|
549
|
+
isInterrupted = true;
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
logger.debug('LIBRARIAN', 'Setting up interruption handlers for group');
|
|
553
|
+
process.on('SIGINT', cleanup);
|
|
554
|
+
process.on('SIGTERM', cleanup);
|
|
555
|
+
|
|
556
|
+
try {
|
|
557
|
+
const technologies = this.config.technologies[groupName];
|
|
558
|
+
if (technologies) {
|
|
559
|
+
const techNames = Object.keys(technologies);
|
|
560
|
+
logger.info('LIBRARIAN', 'Syncing all technologies in group for streaming', { groupName, techCount: techNames.length });
|
|
561
|
+
for (const techName of techNames) {
|
|
562
|
+
await this.syncRepository(techName);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (isInterrupted) {
|
|
567
|
+
logger.warn('LIBRARIAN', 'Group sync interrupted by user', { groupName });
|
|
568
|
+
yield '[Repository sync interrupted by user]';
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const context: AgentContext = {
|
|
573
|
+
workingDir: groupPath,
|
|
574
|
+
group: groupName,
|
|
575
|
+
technology: '',
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
logger.debug('LIBRARIAN', 'Initializing agent for group streaming with context', {
|
|
579
|
+
workingDir: context.workingDir.replace(os.homedir(), '~'),
|
|
580
|
+
group: context.group
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
const agent = new ReactAgent({
|
|
584
|
+
aiProvider: this.config.aiProvider,
|
|
585
|
+
workingDir: groupPath
|
|
586
|
+
});
|
|
587
|
+
await agent.initialize();
|
|
588
|
+
|
|
589
|
+
if (isInterrupted) {
|
|
590
|
+
logger.warn('LIBRARIAN', 'Agent initialization interrupted by user for group', { groupName });
|
|
591
|
+
yield '[Agent initialization interrupted by user]';
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
logger.debug('LIBRARIAN', 'Starting stream from agent for group');
|
|
596
|
+
yield* agent.streamRepository(groupPath, query, context);
|
|
597
|
+
} catch (error) {
|
|
598
|
+
const errorMessage = this.getGroupStreamErrorMessage(error);
|
|
599
|
+
logger.error('LIBRARIAN', 'Group stream error', error instanceof Error ? error : new Error(errorMessage), { groupName });
|
|
600
|
+
yield `\n[Error: ${errorMessage}]`;
|
|
601
|
+
throw error;
|
|
602
|
+
} finally {
|
|
603
|
+
process.removeListener('SIGINT', cleanup);
|
|
604
|
+
process.removeListener('SIGTERM', cleanup);
|
|
605
|
+
logger.timingEnd(timingId, 'LIBRARIAN', `Group stream completed: ${groupName}`);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
private getGroupStreamErrorMessage(error: unknown): string {
|
|
610
|
+
if (!(error instanceof Error)) {
|
|
611
|
+
return 'Unknown error';
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (error.message.includes('not found in configuration')) {
|
|
615
|
+
return error.message;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (error.message.includes('git') || error.message.includes('clone')) {
|
|
619
|
+
return `Repository operation failed: ${error.message}`;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (error.message.includes('timeout')) {
|
|
623
|
+
return 'Repository operation timed out';
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
return `Group error: ${error.message}`;
|
|
627
|
+
}
|
|
628
|
+
}
|