@skroyc/librarian 0.1.0 → 0.2.1

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.
Files changed (76) hide show
  1. package/README.md +4 -16
  2. package/dist/agents/context-schema.d.ts +1 -1
  3. package/dist/agents/context-schema.d.ts.map +1 -1
  4. package/dist/agents/context-schema.js +5 -2
  5. package/dist/agents/context-schema.js.map +1 -1
  6. package/dist/agents/react-agent.d.ts.map +1 -1
  7. package/dist/agents/react-agent.js +63 -170
  8. package/dist/agents/react-agent.js.map +1 -1
  9. package/dist/agents/tool-runtime.d.ts.map +1 -1
  10. package/dist/cli.d.ts +1 -1
  11. package/dist/cli.d.ts.map +1 -1
  12. package/dist/cli.js +53 -49
  13. package/dist/cli.js.map +1 -1
  14. package/dist/config.d.ts +1 -1
  15. package/dist/config.d.ts.map +1 -1
  16. package/dist/config.js +115 -69
  17. package/dist/config.js.map +1 -1
  18. package/dist/index.d.ts +1 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +246 -150
  21. package/dist/index.js.map +1 -1
  22. package/dist/tools/file-finding.tool.d.ts +1 -1
  23. package/dist/tools/file-finding.tool.d.ts.map +1 -1
  24. package/dist/tools/file-finding.tool.js +70 -130
  25. package/dist/tools/file-finding.tool.js.map +1 -1
  26. package/dist/tools/file-listing.tool.d.ts +7 -1
  27. package/dist/tools/file-listing.tool.d.ts.map +1 -1
  28. package/dist/tools/file-listing.tool.js +96 -80
  29. package/dist/tools/file-listing.tool.js.map +1 -1
  30. package/dist/tools/file-reading.tool.d.ts +4 -1
  31. package/dist/tools/file-reading.tool.d.ts.map +1 -1
  32. package/dist/tools/file-reading.tool.js +107 -45
  33. package/dist/tools/file-reading.tool.js.map +1 -1
  34. package/dist/tools/grep-content.tool.d.ts +13 -1
  35. package/dist/tools/grep-content.tool.d.ts.map +1 -1
  36. package/dist/tools/grep-content.tool.js +186 -144
  37. package/dist/tools/grep-content.tool.js.map +1 -1
  38. package/dist/utils/error-utils.d.ts +9 -0
  39. package/dist/utils/error-utils.d.ts.map +1 -0
  40. package/dist/utils/error-utils.js +61 -0
  41. package/dist/utils/error-utils.js.map +1 -0
  42. package/dist/utils/file-utils.d.ts +1 -0
  43. package/dist/utils/file-utils.d.ts.map +1 -1
  44. package/dist/utils/file-utils.js +81 -9
  45. package/dist/utils/file-utils.js.map +1 -1
  46. package/dist/utils/format-utils.d.ts +25 -0
  47. package/dist/utils/format-utils.d.ts.map +1 -0
  48. package/dist/utils/format-utils.js +111 -0
  49. package/dist/utils/format-utils.js.map +1 -0
  50. package/dist/utils/gitignore-service.d.ts +10 -0
  51. package/dist/utils/gitignore-service.d.ts.map +1 -0
  52. package/dist/utils/gitignore-service.js +91 -0
  53. package/dist/utils/gitignore-service.js.map +1 -0
  54. package/dist/utils/logger.d.ts +2 -2
  55. package/dist/utils/logger.d.ts.map +1 -1
  56. package/dist/utils/logger.js +35 -34
  57. package/dist/utils/logger.js.map +1 -1
  58. package/dist/utils/path-utils.js +3 -3
  59. package/dist/utils/path-utils.js.map +1 -1
  60. package/package.json +1 -1
  61. package/src/agents/context-schema.ts +5 -2
  62. package/src/agents/react-agent.ts +694 -784
  63. package/src/agents/tool-runtime.ts +4 -4
  64. package/src/cli.ts +95 -57
  65. package/src/config.ts +192 -90
  66. package/src/index.ts +402 -180
  67. package/src/tools/file-finding.tool.ts +198 -310
  68. package/src/tools/file-listing.tool.ts +245 -202
  69. package/src/tools/file-reading.tool.ts +225 -138
  70. package/src/tools/grep-content.tool.ts +387 -307
  71. package/src/utils/error-utils.ts +95 -0
  72. package/src/utils/file-utils.ts +104 -19
  73. package/src/utils/format-utils.ts +190 -0
  74. package/src/utils/gitignore-service.ts +123 -0
  75. package/src/utils/logger.ts +112 -77
  76. package/src/utils/path-utils.ts +3 -3
package/src/index.ts CHANGED
@@ -3,15 +3,14 @@
3
3
  * Main entry point for the application
4
4
  */
5
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';
6
+ import fs from "node:fs";
7
+ import os from "node:os";
8
+ import path from "node:path";
9
+ import { checkout, clone, fetch } from "isomorphic-git";
10
+ import http from "isomorphic-git/http/node";
11
+ import type { AgentContext } from "./agents/context-schema.js";
12
+ import { ReactAgent } from "./agents/react-agent.js";
13
+ import { logger } from "./utils/logger.js";
15
14
 
16
15
  export interface LibrarianConfig {
17
16
  technologies: {
@@ -24,7 +23,14 @@ export interface LibrarianConfig {
24
23
  };
25
24
  };
26
25
  aiProvider: {
27
- type: 'openai' | 'anthropic' | 'google' | 'openai-compatible' | 'anthropic-compatible' | 'claude-code' | 'gemini-cli';
26
+ type:
27
+ | "openai"
28
+ | "anthropic"
29
+ | "google"
30
+ | "openai-compatible"
31
+ | "anthropic-compatible"
32
+ | "claude-code"
33
+ | "gemini-cli";
28
34
  apiKey: string;
29
35
  model?: string;
30
36
  baseURL?: string;
@@ -39,53 +45,67 @@ export class Librarian {
39
45
  constructor(config: LibrarianConfig) {
40
46
  // Validate AI provider type
41
47
  const validProviderTypes = [
42
- 'openai',
43
- 'anthropic',
44
- 'google',
45
- 'openai-compatible',
46
- 'anthropic-compatible',
47
- 'claude-code',
48
- 'gemini-cli',
48
+ "openai",
49
+ "anthropic",
50
+ "google",
51
+ "openai-compatible",
52
+ "anthropic-compatible",
53
+ "claude-code",
54
+ "gemini-cli",
49
55
  ] 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}`);
56
+ type ValidProviderType = (typeof validProviderTypes)[number];
57
+
58
+ if (
59
+ !validProviderTypes.includes(config.aiProvider.type as ValidProviderType)
60
+ ) {
61
+ throw new Error(
62
+ `Unsupported AI provider type: ${config.aiProvider.type}`
63
+ );
54
64
  }
55
65
 
56
66
  this.config = config;
57
67
 
58
- logger.info('LIBRARIAN', 'Initializing librarian', {
68
+ logger.info("LIBRARIAN", "Initializing librarian", {
59
69
  aiProviderType: config.aiProvider.type,
60
70
  model: config.aiProvider.model,
61
- workingDir: config.workingDir.replace(os.homedir(), '~'),
62
- reposPath: config.repos_path ? config.repos_path.replace(os.homedir(), '~') : 'workingDir'
71
+ workingDir: config.workingDir.replace(os.homedir(), "~"),
72
+ reposPath: config.repos_path
73
+ ? config.repos_path.replace(os.homedir(), "~")
74
+ : "workingDir",
63
75
  });
64
76
  }
65
77
 
66
78
  async initialize(): Promise<void> {
67
79
  // Check if Claude CLI is available if using claude-code provider
68
- if (this.config.aiProvider.type === 'claude-code') {
80
+ if (this.config.aiProvider.type === "claude-code") {
69
81
  try {
70
- const { execSync } = await import('node:child_process');
71
- execSync('claude --version', { stdio: 'ignore' });
72
- logger.info('LIBRARIAN', 'Claude CLI verified');
82
+ const { execSync } = await import("node:child_process");
83
+ execSync("claude --version", { stdio: "ignore" });
84
+ logger.info("LIBRARIAN", "Claude CLI verified");
73
85
  } 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.');
86
+ logger.error("LIBRARIAN", "Claude CLI not found in PATH", undefined, {
87
+ type: "claude-code",
88
+ });
89
+ console.error(
90
+ 'Error: "claude" CLI not found. Please install it to use the "claude-code" provider.'
91
+ );
76
92
  process.exit(1);
77
93
  }
78
94
  }
79
95
 
80
96
  // Check if Gemini CLI is available if using gemini-cli provider
81
- if (this.config.aiProvider.type === 'gemini-cli') {
97
+ if (this.config.aiProvider.type === "gemini-cli") {
82
98
  try {
83
- const { execSync } = await import('node:child_process');
84
- execSync('gemini --version', { stdio: 'ignore' });
85
- logger.info('LIBRARIAN', 'Gemini CLI verified');
99
+ const { execSync } = await import("node:child_process");
100
+ execSync("gemini --version", { stdio: "ignore" });
101
+ logger.info("LIBRARIAN", "Gemini CLI verified");
86
102
  } 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.');
103
+ logger.error("LIBRARIAN", "Gemini CLI not found in PATH", undefined, {
104
+ type: "gemini-cli",
105
+ });
106
+ console.error(
107
+ 'Error: "gemini" CLI not found. Please install it to use the "gemini-cli" provider.'
108
+ );
89
109
  process.exit(1);
90
110
  }
91
111
  }
@@ -93,25 +113,31 @@ export class Librarian {
93
113
  // Create working directory if it doesn't exist
94
114
  const workDir = this.config.repos_path || this.config.workingDir;
95
115
  if (fs.existsSync(workDir)) {
96
- logger.debug('LIBRARIAN', 'Working directory already exists', { path: workDir.replace(os.homedir(), '~') });
116
+ logger.debug("LIBRARIAN", "Working directory already exists", {
117
+ path: workDir.replace(os.homedir(), "~"),
118
+ });
97
119
  } else {
98
- logger.info('LIBRARIAN', 'Creating working directory', { path: workDir.replace(os.homedir(), '~') });
120
+ logger.info("LIBRARIAN", "Creating working directory", {
121
+ path: workDir.replace(os.homedir(), "~"),
122
+ });
99
123
  fs.mkdirSync(workDir, { recursive: true });
100
124
  }
101
125
 
102
- logger.info('LIBRARIAN', 'Initialization complete');
126
+ logger.info("LIBRARIAN", "Initialization complete");
103
127
  }
104
128
 
105
- resolveTechnology(qualifiedName: string): { name: string, group: string, repo: string, branch: string } | undefined {
106
- logger.debug('LIBRARIAN', 'Resolving technology', { qualifiedName });
129
+ resolveTechnology(
130
+ qualifiedName: string
131
+ ): { name: string; group: string; repo: string; branch: string } | undefined {
132
+ logger.debug("LIBRARIAN", "Resolving technology", { qualifiedName });
107
133
 
108
134
  let group: string | undefined;
109
135
  let name: string;
110
136
 
111
- if (qualifiedName.includes(':')) {
112
- const parts = qualifiedName.split(':');
137
+ if (qualifiedName.includes(":")) {
138
+ const parts = qualifiedName.split(":");
113
139
  group = parts[0];
114
- name = parts[1] || '';
140
+ name = parts[1] || "";
115
141
  } else {
116
142
  name = qualifiedName;
117
143
  }
@@ -120,30 +146,63 @@ export class Librarian {
120
146
  const groupTechs = this.config.technologies[group];
121
147
  const tech = groupTechs ? groupTechs[name] : undefined;
122
148
  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' });
149
+ const result = {
150
+ name,
151
+ group,
152
+ repo: tech.repo,
153
+ branch: tech.branch || "main",
154
+ };
155
+ logger.debug("LIBRARIAN", "Technology resolved with explicit group", {
156
+ name,
157
+ group,
158
+ repoHost: tech.repo.split("/")[2] || "unknown",
159
+ });
125
160
  return result;
126
161
  }
127
162
  } else {
128
- for (const [groupName, techs] of Object.entries(this.config.technologies)) {
163
+ for (const [groupName, techs] of Object.entries(
164
+ this.config.technologies
165
+ )) {
129
166
  const tech = techs[name];
130
167
  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' });
168
+ const result = {
169
+ name,
170
+ group: groupName,
171
+ repo: tech.repo,
172
+ branch: tech.branch || "main",
173
+ };
174
+ logger.debug("LIBRARIAN", "Technology resolved by search", {
175
+ name,
176
+ group: groupName,
177
+ repoHost: tech.repo.split("/")[2] || "unknown",
178
+ });
133
179
  return result;
134
180
  }
135
181
  }
136
182
  }
137
- logger.debug('LIBRARIAN', 'Technology not found in configuration', { qualifiedName });
183
+ logger.debug("LIBRARIAN", "Technology not found in configuration", {
184
+ qualifiedName,
185
+ });
138
186
  return undefined;
139
187
  }
140
188
 
141
189
  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`);
190
+ logger.debug("PATH", "Validating group path", { groupName });
191
+
192
+ if (
193
+ groupName.includes("../") ||
194
+ groupName.includes("..\\") ||
195
+ groupName.startsWith("..")
196
+ ) {
197
+ logger.error(
198
+ "PATH",
199
+ "Group name contains path traversal characters",
200
+ undefined,
201
+ { groupName }
202
+ );
203
+ throw new Error(
204
+ `Group name "${groupName}" contains invalid path characters`
205
+ );
147
206
  }
148
207
 
149
208
  const sanitizedGroupName = path.basename(groupName);
@@ -154,61 +213,104 @@ export class Librarian {
154
213
  const resolvedGroupPath = path.resolve(groupPath);
155
214
 
156
215
  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`);
216
+ logger.error(
217
+ "PATH",
218
+ "Group path escapes working directory sandbox",
219
+ undefined,
220
+ { groupName }
221
+ );
222
+ throw new Error(
223
+ `Group name "${groupName}" attempts to escape the working directory sandbox`
224
+ );
159
225
  }
160
226
 
161
- logger.debug('PATH', 'Group path validated', { groupName, path: groupPath.replace(os.homedir(), '~') });
227
+ logger.debug("PATH", "Group path validated", {
228
+ groupName,
229
+ path: groupPath.replace(os.homedir(), "~"),
230
+ });
162
231
  return groupPath;
163
232
  }
164
233
 
165
- private getSecureRepoPath(repoName: string, groupName = 'default'): string {
166
- logger.debug('PATH', 'Validating repo path', { repoName, groupName });
234
+ private getSecureRepoPath(repoName: string, groupName = "default"): string {
235
+ logger.debug("PATH", "Validating repo path", { repoName, groupName });
167
236
 
168
237
  // 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`);
238
+ if (
239
+ repoName.includes("../") ||
240
+ repoName.includes("..\\") ||
241
+ repoName.startsWith("..")
242
+ ) {
243
+ logger.error(
244
+ "PATH",
245
+ "Repo name contains path traversal characters",
246
+ undefined,
247
+ { repoName, groupName }
248
+ );
249
+ throw new Error(
250
+ `Repository name "${repoName}" contains invalid path characters`
251
+ );
172
252
  }
173
-
253
+
174
254
  // Sanitize the names
175
255
  const sanitizedRepoName = path.basename(repoName);
176
256
  const groupPath = this.getSecureGroupPath(groupName);
177
257
  const repoPath = path.join(groupPath, sanitizedRepoName);
178
-
258
+
179
259
  // Verify that the resulting path is within the group directory (which is already sandboxed)
180
260
  const resolvedGroupDir = path.resolve(groupPath);
181
261
  const resolvedRepoPath = path.resolve(repoPath);
182
-
262
+
183
263
  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`);
264
+ logger.error("PATH", "Repo path escapes group sandbox", undefined, {
265
+ repoName,
266
+ groupName,
267
+ });
268
+ throw new Error(
269
+ `Repository name "${repoName}" attempts to escape the group sandbox`
270
+ );
186
271
  }
187
272
 
188
- logger.debug('PATH', 'Repo path validated', { repoName, groupName, path: repoPath.replace(os.homedir(), '~') });
273
+ logger.debug("PATH", "Repo path validated", {
274
+ repoName,
275
+ groupName,
276
+ path: repoPath.replace(os.homedir(), "~"),
277
+ });
189
278
  return repoPath;
190
279
  }
191
280
 
192
- async updateRepository(repoName: string, groupName = 'default'): Promise<void> {
193
- logger.info('GIT', 'Updating repository', { repoName, groupName });
281
+ async updateRepository(
282
+ repoName: string,
283
+ groupName = "default"
284
+ ): Promise<void> {
285
+ logger.info("GIT", "Updating repository", { repoName, groupName });
194
286
 
195
- const timingId = logger.timingStart('updateRepository');
287
+ const timingId = logger.timingStart("updateRepository");
196
288
 
197
289
  const repoPath = this.getSecureRepoPath(repoName, groupName);
198
- const gitPath = path.join(repoPath, '.git');
290
+ const gitPath = path.join(repoPath, ".git");
199
291
 
200
292
  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.`);
293
+ logger.error("GIT", "Repository path does not exist", undefined, {
294
+ repoName,
295
+ repoPath: repoPath.replace(os.homedir(), "~"),
296
+ });
297
+ throw new Error(
298
+ `Repository ${repoName} does not exist at ${repoPath}. Cannot update.`
299
+ );
203
300
  }
204
301
 
205
302
  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.`);
303
+ logger.error("GIT", "Directory is not a git repository", undefined, {
304
+ repoName,
305
+ repoPath: repoPath.replace(os.homedir(), "~"),
306
+ });
307
+ throw new Error(
308
+ `Directory ${repoName} exists at ${repoPath} but is not a git repository. Cannot update.`
309
+ );
208
310
  }
209
311
 
210
312
  // Fetch updates from the remote
211
- logger.debug('GIT', 'Fetching updates from remote');
313
+ logger.debug("GIT", "Fetching updates from remote");
212
314
  await fetch({
213
315
  fs,
214
316
  http,
@@ -216,75 +318,104 @@ export class Librarian {
216
318
  singleBranch: true,
217
319
  });
218
320
 
219
- const tech = this.resolveTechnology(`${groupName}:${repoName}`) || this.resolveTechnology(repoName);
220
- const branch = tech?.branch || 'main';
321
+ const tech =
322
+ this.resolveTechnology(`${groupName}:${repoName}`) ||
323
+ this.resolveTechnology(repoName);
324
+ const branch = tech?.branch || "main";
221
325
 
222
326
  // Checkout the latest version
223
- logger.debug('GIT', 'Checking out branch', { branch });
327
+ logger.debug("GIT", "Checking out branch", { branch });
224
328
  await checkout({
225
329
  fs,
226
330
  dir: repoPath,
227
331
  ref: `origin/${branch}`,
228
332
  });
229
333
 
230
- logger.timingEnd(timingId, 'GIT', `Repository updated: ${repoName}`);
334
+ logger.timingEnd(timingId, "GIT", `Repository updated: ${repoName}`);
231
335
  }
232
336
 
233
337
  async syncRepository(repoName: string): Promise<string> {
234
- logger.info('GIT', 'Syncing repository', { repoName });
338
+ logger.info("GIT", "Syncing repository", { repoName });
235
339
 
236
340
  const tech = this.resolveTechnology(repoName);
237
341
 
238
342
  if (!tech) {
239
- logger.error('GIT', 'Technology not found in configuration', undefined, { repoName });
343
+ logger.error("GIT", "Technology not found in configuration", undefined, {
344
+ repoName,
345
+ });
240
346
  throw new Error(`Repository ${repoName} not found in configuration`);
241
347
  }
242
348
 
243
349
  const repoPath = this.getSecureRepoPath(tech.name, tech.group);
244
350
 
245
351
  // Check if this is a local path (not a remote URL)
246
- const isLocalRepo = !(tech.repo.startsWith('http://') || tech.repo.startsWith('https://'));
352
+ const isLocalRepo = !(
353
+ tech.repo.startsWith("http://") || tech.repo.startsWith("https://")
354
+ );
247
355
 
248
356
  if (fs.existsSync(repoPath)) {
249
357
  // Repository exists
250
358
  if (isLocalRepo) {
251
359
  // For local repos, skip git operations - just use existing files
252
- logger.debug('GIT', 'Local repository exists, skipping git operations');
360
+ logger.debug("GIT", "Local repository exists, skipping git operations");
253
361
  return repoPath;
254
362
  }
255
363
  // Remote repository, update it (no-op for local repos to avoid isomorphic-git issues with local paths)
256
364
  if (!isLocalRepo) {
257
- logger.debug('GIT', 'Repository exists, performing update');
365
+ logger.debug("GIT", "Repository exists, performing update");
258
366
  await this.updateRepository(tech.name, tech.group);
259
367
  }
260
368
  } else {
261
369
  // Repository doesn't exist
262
370
  if (isLocalRepo) {
263
371
  // 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}`);
372
+ logger.error("GIT", "Local repository path does not exist", undefined, {
373
+ repoName,
374
+ repoPath,
375
+ });
376
+ throw new Error(
377
+ `Local repository ${repoName} does not exist at ${repoPath}`
378
+ );
266
379
  }
267
380
  // 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);
381
+ logger.debug("GIT", "Repository does not exist, performing clone");
382
+ return await this.cloneRepository(
383
+ tech.name,
384
+ tech.repo,
385
+ tech.group,
386
+ tech.branch
387
+ );
270
388
  }
271
389
 
272
390
  return repoPath;
273
391
  }
274
392
 
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 });
393
+ async cloneRepository(
394
+ repoName: string,
395
+ repoUrl: string,
396
+ groupName = "default",
397
+ branch = "main"
398
+ ): Promise<string> {
399
+ logger.info("GIT", "Cloning repository", {
400
+ repoName,
401
+ repoHost: repoUrl.split("/")[2] || "unknown",
402
+ branch,
403
+ groupName,
404
+ });
277
405
 
278
- const timingId = logger.timingStart('cloneRepository');
406
+ const timingId = logger.timingStart("cloneRepository");
279
407
 
280
408
  const repoPath = this.getSecureRepoPath(repoName, groupName);
281
409
 
282
410
  // Check if repository already exists
283
411
  if (fs.existsSync(repoPath)) {
284
412
  // Check if it's a git repository by checking for .git folder
285
- const gitPath = path.join(repoPath, '.git');
413
+ const gitPath = path.join(repoPath, ".git");
286
414
  if (fs.existsSync(gitPath)) {
287
- logger.debug('GIT', 'Repository already exists as git repo, updating instead');
415
+ logger.debug(
416
+ "GIT",
417
+ "Repository already exists as git repo, updating instead"
418
+ );
288
419
  await this.updateRepository(repoName, groupName);
289
420
  return repoPath;
290
421
  }
@@ -293,11 +424,14 @@ export class Librarian {
293
424
  // Ensure group directory exists
294
425
  const groupPath = this.getSecureGroupPath(groupName);
295
426
  if (!fs.existsSync(groupPath)) {
296
- logger.debug('GIT', 'Creating group directory', { groupName, path: groupPath.replace(os.homedir(), '~') });
427
+ logger.debug("GIT", "Creating group directory", {
428
+ groupName,
429
+ path: groupPath.replace(os.homedir(), "~"),
430
+ });
297
431
  fs.mkdirSync(groupPath, { recursive: true });
298
432
  }
299
433
 
300
- logger.debug('GIT', 'Starting shallow clone', { depth: 1 });
434
+ logger.debug("GIT", "Starting shallow clone", { depth: 1 });
301
435
  await clone({
302
436
  fs,
303
437
  http,
@@ -314,7 +448,7 @@ export class Librarian {
314
448
  try {
315
449
  const files = fs.readdirSync(dir, { withFileTypes: true });
316
450
  for (const file of files) {
317
- if (file.name === '.git') {
451
+ if (file.name === ".git") {
318
452
  continue;
319
453
  }
320
454
  if (file.isDirectory()) {
@@ -329,21 +463,29 @@ export class Librarian {
329
463
  };
330
464
  countFiles(repoPath);
331
465
 
332
- logger.debug('GIT', 'Clone completed', { fileCount });
466
+ logger.debug("GIT", "Clone completed", { fileCount });
333
467
 
334
- logger.timingEnd(timingId, 'GIT', `Repository cloned: ${repoName}`);
468
+ logger.timingEnd(timingId, "GIT", `Repository cloned: ${repoName}`);
335
469
  return repoPath;
336
470
  }
337
471
 
338
472
  async queryRepository(repoName: string, query: string): Promise<string> {
339
- logger.info('LIBRARIAN', 'Querying repository', { repoName, queryLength: query.length });
473
+ logger.info("LIBRARIAN", "Querying repository", {
474
+ repoName,
475
+ queryLength: query.length,
476
+ });
340
477
 
341
- const timingId = logger.timingStart('queryRepository');
478
+ const timingId = logger.timingStart("queryRepository");
342
479
 
343
480
  const tech = this.resolveTechnology(repoName);
344
481
 
345
482
  if (!tech) {
346
- logger.error('LIBRARIAN', 'Technology not found in configuration', undefined, { repoName });
483
+ logger.error(
484
+ "LIBRARIAN",
485
+ "Technology not found in configuration",
486
+ undefined,
487
+ { repoName }
488
+ );
347
489
  throw new Error(`Repository ${repoName} not found in configuration`);
348
490
  }
349
491
 
@@ -357,10 +499,10 @@ export class Librarian {
357
499
  technology: tech.name,
358
500
  };
359
501
 
360
- logger.debug('LIBRARIAN', 'Initializing agent for query with context', {
361
- workingDir: context.workingDir.replace(os.homedir(), '~'),
502
+ logger.debug("LIBRARIAN", "Initializing agent for query with context", {
503
+ workingDir: context.workingDir.replace(os.homedir(), "~"),
362
504
  group: context.group,
363
- technology: context.technology
505
+ technology: context.technology,
364
506
  });
365
507
 
366
508
  // Initialize the agent
@@ -370,29 +512,43 @@ export class Librarian {
370
512
  technology: {
371
513
  name: tech.name,
372
514
  repository: tech.repo,
373
- branch: tech.branch
374
- }
515
+ branch: tech.branch,
516
+ },
375
517
  });
376
518
  await agent.initialize();
377
519
 
378
520
  // Execute the query using the agent with context
379
521
  const result = await agent.queryRepository(repoPath, query, context);
380
522
 
381
- logger.timingEnd(timingId, 'LIBRARIAN', `Query completed: ${repoName}`);
382
- logger.info('LIBRARIAN', 'Query result received', { repoName, responseLength: result.length });
523
+ logger.timingEnd(timingId, "LIBRARIAN", `Query completed: ${repoName}`);
524
+ logger.info("LIBRARIAN", "Query result received", {
525
+ repoName,
526
+ responseLength: result.length,
527
+ });
383
528
 
384
529
  return result;
385
530
  }
386
531
 
387
- async *streamRepository(repoName: string, query: string): AsyncGenerator<string, void, unknown> {
388
- logger.info('LIBRARIAN', 'Streaming repository query', { repoName, queryLength: query.length });
532
+ async *streamRepository(
533
+ repoName: string,
534
+ query: string
535
+ ): AsyncGenerator<string, void, unknown> {
536
+ logger.info("LIBRARIAN", "Streaming repository query", {
537
+ repoName,
538
+ queryLength: query.length,
539
+ });
389
540
 
390
- const timingId = logger.timingStart('streamRepository');
541
+ const timingId = logger.timingStart("streamRepository");
391
542
 
392
543
  const tech = this.resolveTechnology(repoName);
393
544
 
394
545
  if (!tech) {
395
- logger.error('LIBRARIAN', 'Technology not found in configuration', undefined, { repoName });
546
+ logger.error(
547
+ "LIBRARIAN",
548
+ "Technology not found in configuration",
549
+ undefined,
550
+ { repoName }
551
+ );
396
552
  throw new Error(`Repository ${repoName} not found in configuration`);
397
553
  }
398
554
 
@@ -403,9 +559,9 @@ export class Librarian {
403
559
  };
404
560
 
405
561
  // Listen for interruption signals (Ctrl+C)
406
- logger.debug('LIBRARIAN', 'Setting up interruption handlers');
407
- process.on('SIGINT', cleanup);
408
- process.on('SIGTERM', cleanup);
562
+ logger.debug("LIBRARIAN", "Setting up interruption handlers");
563
+ process.on("SIGINT", cleanup);
564
+ process.on("SIGTERM", cleanup);
409
565
 
410
566
  try {
411
567
  // Clone or sync repository first
@@ -413,8 +569,10 @@ export class Librarian {
413
569
 
414
570
  // Check for interruption after sync
415
571
  if (isInterrupted) {
416
- logger.warn('LIBRARIAN', 'Repository sync interrupted by user', { repoName });
417
- yield '[Repository sync interrupted by user]';
572
+ logger.warn("LIBRARIAN", "Repository sync interrupted by user", {
573
+ repoName,
574
+ });
575
+ yield "[Repository sync interrupted by user]";
418
576
  return;
419
577
  }
420
578
 
@@ -425,11 +583,15 @@ export class Librarian {
425
583
  technology: tech.name,
426
584
  };
427
585
 
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
- });
586
+ logger.debug(
587
+ "LIBRARIAN",
588
+ "Initializing agent for streaming query with context",
589
+ {
590
+ workingDir: context.workingDir.replace(os.homedir(), "~"),
591
+ group: context.group,
592
+ technology: context.technology,
593
+ }
594
+ );
433
595
 
434
596
  // Initialize agent
435
597
  const agent = new ReactAgent({
@@ -438,56 +600,71 @@ export class Librarian {
438
600
  technology: {
439
601
  name: tech.name,
440
602
  repository: tech.repo,
441
- branch: tech.branch
442
- }
603
+ branch: tech.branch,
604
+ },
443
605
  });
444
606
  await agent.initialize();
445
607
 
446
608
  // Check for interruption after initialization
447
609
  if (isInterrupted) {
448
- logger.warn('LIBRARIAN', 'Agent initialization interrupted by user', { repoName });
449
- yield '[Agent initialization interrupted by user]';
610
+ logger.warn("LIBRARIAN", "Agent initialization interrupted by user", {
611
+ repoName,
612
+ });
613
+ yield "[Agent initialization interrupted by user]";
450
614
  return;
451
615
  }
452
616
 
453
617
  // Execute streaming query using agent with context
454
- logger.debug('LIBRARIAN', 'Starting stream from agent');
618
+ logger.debug("LIBRARIAN", "Starting stream from agent");
455
619
  yield* agent.streamRepository(repoPath, query, context);
456
620
  } catch (error) {
457
621
  // Handle repository-level errors
458
- let errorMessage = 'Unknown error';
622
+ let errorMessage = "Unknown error";
459
623
 
460
624
  if (error instanceof Error) {
461
- if (error.message.includes('not found in configuration')) {
625
+ if (error.message.includes("not found in configuration")) {
462
626
  errorMessage = error.message;
463
- } else if (error.message.includes('git') || error.message.includes('clone')) {
627
+ } else if (
628
+ error.message.includes("git") ||
629
+ error.message.includes("clone")
630
+ ) {
464
631
  errorMessage = `Repository operation failed: ${error.message}`;
465
- } else if (error.message.includes('timeout')) {
466
- errorMessage = 'Repository operation timed out';
632
+ } else if (error.message.includes("timeout")) {
633
+ errorMessage = "Repository operation timed out";
467
634
  } else {
468
635
  errorMessage = `Repository error: ${error.message}`;
469
636
  }
470
637
  }
471
638
 
472
- logger.error('LIBRARIAN', 'Stream error', error instanceof Error ? error : new Error(errorMessage), { repoName });
639
+ logger.error(
640
+ "LIBRARIAN",
641
+ "Stream error",
642
+ error instanceof Error ? error : new Error(errorMessage),
643
+ { repoName }
644
+ );
473
645
  yield `\n[Error: ${errorMessage}]`;
474
646
  throw error;
475
647
  } finally {
476
648
  // Clean up event listeners
477
- process.removeListener('SIGINT', cleanup);
478
- process.removeListener('SIGTERM', cleanup);
479
- logger.timingEnd(timingId, 'LIBRARIAN', `Stream completed: ${repoName}`);
649
+ process.removeListener("SIGINT", cleanup);
650
+ process.removeListener("SIGTERM", cleanup);
651
+ logger.timingEnd(timingId, "LIBRARIAN", `Stream completed: ${repoName}`);
480
652
  }
481
653
  }
482
654
 
483
655
  async queryGroup(groupName: string, query: string): Promise<string> {
484
- logger.info('LIBRARIAN', 'Querying group', { groupName, queryLength: query.length });
656
+ logger.info("LIBRARIAN", "Querying group", {
657
+ groupName,
658
+ queryLength: query.length,
659
+ });
485
660
 
486
- const timingId = logger.timingStart('queryGroup');
661
+ const timingId = logger.timingStart("queryGroup");
487
662
 
488
663
  // Validate group exists
489
664
  if (!this.config.technologies[groupName]) {
490
- logger.error('LIBRARIAN', 'Group not found in configuration', undefined, { groupName });
665
+ logger.error("LIBRARIAN", "Group not found in configuration", undefined, {
666
+ groupName,
667
+ });
491
668
  throw new Error(`Group ${groupName} not found in configuration`);
492
669
  }
493
670
 
@@ -498,7 +675,10 @@ export class Librarian {
498
675
  const technologies = this.config.technologies[groupName];
499
676
  if (technologies) {
500
677
  const techNames = Object.keys(technologies);
501
- logger.info('LIBRARIAN', 'Syncing all technologies in group', { groupName, techCount: techNames.length });
678
+ logger.info("LIBRARIAN", "Syncing all technologies in group", {
679
+ groupName,
680
+ techCount: techNames.length,
681
+ });
502
682
  for (const techName of techNames) {
503
683
  await this.syncRepository(techName);
504
684
  }
@@ -508,37 +688,56 @@ export class Librarian {
508
688
  const context: AgentContext = {
509
689
  workingDir: groupPath,
510
690
  group: groupName,
511
- technology: '', // No specific technology for group-level queries
691
+ technology: "", // No specific technology for group-level queries
512
692
  };
513
693
 
514
- logger.debug('LIBRARIAN', 'Initializing agent for group query with context', {
515
- workingDir: context.workingDir.replace(os.homedir(), '~'),
516
- group: context.group
517
- });
694
+ logger.debug(
695
+ "LIBRARIAN",
696
+ "Initializing agent for group query with context",
697
+ {
698
+ workingDir: context.workingDir.replace(os.homedir(), "~"),
699
+ group: context.group,
700
+ }
701
+ );
518
702
 
519
703
  // Initialize the agent with the group directory as working directory
520
704
  const agent = new ReactAgent({
521
705
  aiProvider: this.config.aiProvider,
522
- workingDir: groupPath
706
+ workingDir: groupPath,
523
707
  });
524
708
  await agent.initialize();
525
709
 
526
710
  // Execute the query using the agent with context
527
711
  const result = await agent.queryRepository(groupPath, query, context);
528
712
 
529
- logger.timingEnd(timingId, 'LIBRARIAN', `Group query completed: ${groupName}`);
530
- logger.info('LIBRARIAN', 'Group query result received', { groupName, responseLength: result.length });
713
+ logger.timingEnd(
714
+ timingId,
715
+ "LIBRARIAN",
716
+ `Group query completed: ${groupName}`
717
+ );
718
+ logger.info("LIBRARIAN", "Group query result received", {
719
+ groupName,
720
+ responseLength: result.length,
721
+ });
531
722
 
532
723
  return result;
533
724
  }
534
725
 
535
- async *streamGroup(groupName: string, query: string): AsyncGenerator<string, void, unknown> {
536
- logger.info('LIBRARIAN', 'Streaming group query', { groupName, queryLength: query.length });
726
+ async *streamGroup(
727
+ groupName: string,
728
+ query: string
729
+ ): AsyncGenerator<string, void, unknown> {
730
+ logger.info("LIBRARIAN", "Streaming group query", {
731
+ groupName,
732
+ queryLength: query.length,
733
+ });
537
734
 
538
- const timingId = logger.timingStart('streamGroup');
735
+ const timingId = logger.timingStart("streamGroup");
539
736
 
540
737
  if (!this.config.technologies[groupName]) {
541
- logger.error('LIBRARIAN', 'Group not found in configuration', undefined, { groupName });
738
+ logger.error("LIBRARIAN", "Group not found in configuration", undefined, {
739
+ groupName,
740
+ });
542
741
  throw new Error(`Group ${groupName} not found in configuration`);
543
742
  }
544
743
 
@@ -549,80 +748,103 @@ export class Librarian {
549
748
  isInterrupted = true;
550
749
  };
551
750
 
552
- logger.debug('LIBRARIAN', 'Setting up interruption handlers for group');
553
- process.on('SIGINT', cleanup);
554
- process.on('SIGTERM', cleanup);
751
+ logger.debug("LIBRARIAN", "Setting up interruption handlers for group");
752
+ process.on("SIGINT", cleanup);
753
+ process.on("SIGTERM", cleanup);
555
754
 
556
755
  try {
557
756
  const technologies = this.config.technologies[groupName];
558
757
  if (technologies) {
559
758
  const techNames = Object.keys(technologies);
560
- logger.info('LIBRARIAN', 'Syncing all technologies in group for streaming', { groupName, techCount: techNames.length });
759
+ logger.info(
760
+ "LIBRARIAN",
761
+ "Syncing all technologies in group for streaming",
762
+ { groupName, techCount: techNames.length }
763
+ );
561
764
  for (const techName of techNames) {
562
765
  await this.syncRepository(techName);
563
766
  }
564
767
  }
565
768
 
566
769
  if (isInterrupted) {
567
- logger.warn('LIBRARIAN', 'Group sync interrupted by user', { groupName });
568
- yield '[Repository sync interrupted by user]';
770
+ logger.warn("LIBRARIAN", "Group sync interrupted by user", {
771
+ groupName,
772
+ });
773
+ yield "[Repository sync interrupted by user]";
569
774
  return;
570
775
  }
571
776
 
572
777
  const context: AgentContext = {
573
778
  workingDir: groupPath,
574
779
  group: groupName,
575
- technology: '',
780
+ technology: "",
576
781
  };
577
782
 
578
- logger.debug('LIBRARIAN', 'Initializing agent for group streaming with context', {
579
- workingDir: context.workingDir.replace(os.homedir(), '~'),
580
- group: context.group
581
- });
783
+ logger.debug(
784
+ "LIBRARIAN",
785
+ "Initializing agent for group streaming with context",
786
+ {
787
+ workingDir: context.workingDir.replace(os.homedir(), "~"),
788
+ group: context.group,
789
+ }
790
+ );
582
791
 
583
792
  const agent = new ReactAgent({
584
793
  aiProvider: this.config.aiProvider,
585
- workingDir: groupPath
794
+ workingDir: groupPath,
586
795
  });
587
796
  await agent.initialize();
588
797
 
589
798
  if (isInterrupted) {
590
- logger.warn('LIBRARIAN', 'Agent initialization interrupted by user for group', { groupName });
591
- yield '[Agent initialization interrupted by user]';
799
+ logger.warn(
800
+ "LIBRARIAN",
801
+ "Agent initialization interrupted by user for group",
802
+ { groupName }
803
+ );
804
+ yield "[Agent initialization interrupted by user]";
592
805
  return;
593
806
  }
594
807
 
595
- logger.debug('LIBRARIAN', 'Starting stream from agent for group');
808
+ logger.debug("LIBRARIAN", "Starting stream from agent for group");
596
809
  yield* agent.streamRepository(groupPath, query, context);
597
810
  } catch (error) {
598
811
  const errorMessage = this.getGroupStreamErrorMessage(error);
599
- logger.error('LIBRARIAN', 'Group stream error', error instanceof Error ? error : new Error(errorMessage), { groupName });
812
+ logger.error(
813
+ "LIBRARIAN",
814
+ "Group stream error",
815
+ error instanceof Error ? error : new Error(errorMessage),
816
+ { groupName }
817
+ );
600
818
  yield `\n[Error: ${errorMessage}]`;
601
819
  throw error;
602
820
  } finally {
603
- process.removeListener('SIGINT', cleanup);
604
- process.removeListener('SIGTERM', cleanup);
605
- logger.timingEnd(timingId, 'LIBRARIAN', `Group stream completed: ${groupName}`);
821
+ process.removeListener("SIGINT", cleanup);
822
+ process.removeListener("SIGTERM", cleanup);
823
+ logger.timingEnd(
824
+ timingId,
825
+ "LIBRARIAN",
826
+ `Group stream completed: ${groupName}`
827
+ );
606
828
  }
607
829
  }
608
830
 
609
831
  private getGroupStreamErrorMessage(error: unknown): string {
610
832
  if (!(error instanceof Error)) {
611
- return 'Unknown error';
833
+ return "Unknown error";
612
834
  }
613
835
 
614
- if (error.message.includes('not found in configuration')) {
836
+ if (error.message.includes("not found in configuration")) {
615
837
  return error.message;
616
838
  }
617
839
 
618
- if (error.message.includes('git') || error.message.includes('clone')) {
840
+ if (error.message.includes("git") || error.message.includes("clone")) {
619
841
  return `Repository operation failed: ${error.message}`;
620
842
  }
621
843
 
622
- if (error.message.includes('timeout')) {
623
- return 'Repository operation timed out';
844
+ if (error.message.includes("timeout")) {
845
+ return "Repository operation timed out";
624
846
  }
625
847
 
626
848
  return `Group error: ${error.message}`;
627
849
  }
628
- }
850
+ }