@skroyc/librarian 0.1.0 → 0.2.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.
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 +36 -27
  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 +667 -641
  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/config.ts CHANGED
@@ -1,16 +1,16 @@
1
- import { mkdir } from 'node:fs/promises';
2
- import path from 'node:path';
3
- import { parse, stringify } from 'yaml';
4
- import { z } from 'zod';
5
- import type { LibrarianConfig } from './index.js';
6
- import { logger } from './utils/logger.js';
7
- import { expandTilde } from './utils/path-utils.js';
8
- import os from 'node:os';
1
+ import { mkdir } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { parse, stringify } from "yaml";
5
+ import { z } from "zod";
6
+ import type { LibrarianConfig } from "./index.js";
7
+ import { logger } from "./utils/logger.js";
8
+ import { expandTilde } from "./utils/path-utils.js";
9
9
 
10
10
  const TechnologySchema = z.object({
11
11
  repo: z.string().optional(),
12
12
  name: z.string().optional(), // For README style
13
- branch: z.string().default('main'),
13
+ branch: z.string().default("main"),
14
14
  description: z.string().optional(),
15
15
  });
16
16
 
@@ -19,17 +19,37 @@ const GroupSchema = z.record(z.string(), TechnologySchema);
19
19
  const ConfigSchema = z.object({
20
20
  technologies: z.record(z.string(), GroupSchema).optional(),
21
21
  repositories: z.record(z.string(), z.string()).optional(), // For backward compatibility
22
- aiProvider: z.object({
23
- type: z.enum(['openai', 'anthropic', 'google', 'openai-compatible', 'anthropic-compatible', 'claude-code', 'gemini-cli']),
24
- apiKey: z.string().optional(), // Optional - will be loaded from .env or not needed for claude-code/gemini-cli
25
- model: z.string().optional(),
26
- baseURL: z.string().optional(),
27
- }).optional(),
22
+ aiProvider: z
23
+ .object({
24
+ type: z.enum([
25
+ "openai",
26
+ "anthropic",
27
+ "google",
28
+ "openai-compatible",
29
+ "anthropic-compatible",
30
+ "claude-code",
31
+ "gemini-cli",
32
+ ]),
33
+ apiKey: z.string().optional(), // Optional - will be loaded from .env or not needed for claude-code/gemini-cli
34
+ model: z.string().optional(),
35
+ baseURL: z.string().optional(),
36
+ })
37
+ .optional(),
28
38
  // Support README style keys
29
- llm_provider: z.enum(['openai', 'anthropic', 'google', 'openai-compatible', 'anthropic-compatible', 'claude-code', 'gemini-cli']).optional(),
39
+ llm_provider: z
40
+ .enum([
41
+ "openai",
42
+ "anthropic",
43
+ "google",
44
+ "openai-compatible",
45
+ "anthropic-compatible",
46
+ "claude-code",
47
+ "gemini-cli",
48
+ ])
49
+ .optional(),
30
50
  llm_model: z.string().optional(),
31
51
  base_url: z.string().optional(),
32
- workingDir: z.string().default('./librarian_work'),
52
+ workingDir: z.string().default("./librarian_work"),
33
53
  repos_path: z.string().optional(),
34
54
  });
35
55
 
@@ -38,33 +58,35 @@ const ConfigSchema = z.object({
38
58
  * Supports simple KEY=VALUE format, comments (#), and quoted values
39
59
  */
40
60
  async function loadEnvFile(envPath: string): Promise<Record<string, string>> {
41
- logger.debug('CONFIG', "Loading .env file", { envPath: envPath.replace(os.homedir(), '~') });
61
+ logger.debug("CONFIG", "Loading .env file", {
62
+ envPath: envPath.replace(os.homedir(), "~"),
63
+ });
42
64
 
43
65
  const envFile = Bun.file(envPath);
44
66
 
45
67
  // Check if .env file exists
46
68
  if (!(await envFile.exists())) {
47
- logger.debug('CONFIG', '.env file not found, continuing without it');
69
+ logger.debug("CONFIG", ".env file not found, continuing without it");
48
70
  return {};
49
71
  }
50
72
 
51
- logger.info('CONFIG', '.env file found and loading');
73
+ logger.info("CONFIG", ".env file found and loading");
52
74
 
53
75
  // Read file content
54
76
  const content = await envFile.text();
55
77
  const env: Record<string, string> = {};
56
78
 
57
79
  // Parse line by line
58
- for (const line of content.split('\n')) {
80
+ for (const line of content.split("\n")) {
59
81
  const trimmed = line.trim();
60
82
 
61
83
  // Skip empty lines and comments
62
- if (!trimmed || trimmed.startsWith('#')) {
84
+ if (!trimmed || trimmed.startsWith("#")) {
63
85
  continue;
64
86
  }
65
87
 
66
88
  // Parse KEY=VALUE
67
- const equalsIndex = trimmed.indexOf('=');
89
+ const equalsIndex = trimmed.indexOf("=");
68
90
  if (equalsIndex === -1) {
69
91
  continue; // Skip malformed lines
70
92
  }
@@ -73,58 +95,88 @@ async function loadEnvFile(envPath: string): Promise<Record<string, string>> {
73
95
  let value = trimmed.slice(equalsIndex + 1).trim();
74
96
 
75
97
  // Remove quotes from value
76
- if ((value.startsWith('"') && value.endsWith('"')) ||
77
- (value.startsWith("'") && value.endsWith("'"))) {
98
+ if (
99
+ (value.startsWith('"') && value.endsWith('"')) ||
100
+ (value.startsWith("'") && value.endsWith("'"))
101
+ ) {
78
102
  value = value.slice(1, -1);
79
103
  }
80
104
 
81
105
  env[key] = value;
82
106
  }
83
107
 
84
- logger.debug('CONFIG', `.env loaded: ${Object.keys(env).length} variables`);
108
+ logger.debug("CONFIG", `.env loaded: ${Object.keys(env).length} variables`);
85
109
  return env;
86
110
  }
87
111
 
88
- function validateApiKey(config: LibrarianConfig, envPath: string, errors: string[]): void {
89
- const isCliProvider = config.aiProvider.type === 'claude-code' || config.aiProvider.type === 'gemini-cli';
90
- if (!isCliProvider && (!config.aiProvider.apiKey || config.aiProvider.apiKey.trim() === '')) {
112
+ function validateApiKey(
113
+ config: LibrarianConfig,
114
+ envPath: string,
115
+ errors: string[]
116
+ ): void {
117
+ const isCliProvider =
118
+ config.aiProvider.type === "claude-code" ||
119
+ config.aiProvider.type === "gemini-cli";
120
+ if (
121
+ !isCliProvider &&
122
+ (!config.aiProvider.apiKey || config.aiProvider.apiKey.trim() === "")
123
+ ) {
91
124
  const errorMsg = `API key is missing or empty. Please set LIBRARIAN_API_KEY in ${envPath}`;
92
125
  errors.push(errorMsg);
93
- logger.debug('CONFIG', 'Validation failed: API key missing', { envPath: envPath.replace(os.homedir(), '~') });
126
+ logger.debug("CONFIG", "Validation failed: API key missing", {
127
+ envPath: envPath.replace(os.homedir(), "~"),
128
+ });
94
129
  }
95
130
  }
96
131
 
97
- function validateBaseUrlForCompatibleProviders(config: LibrarianConfig, errors: string[]): void {
98
- if (config.aiProvider.type === 'openai-compatible' && !config.aiProvider.baseURL) {
99
- const errorMsg = 'base_url is required for openai-compatible providers';
132
+ function validateBaseUrlForCompatibleProviders(
133
+ config: LibrarianConfig,
134
+ errors: string[]
135
+ ): void {
136
+ if (
137
+ config.aiProvider.type === "openai-compatible" &&
138
+ !config.aiProvider.baseURL
139
+ ) {
140
+ const errorMsg = "base_url is required for openai-compatible providers";
100
141
  errors.push(errorMsg);
101
- logger.debug('CONFIG', 'Validation failed: base_url missing for openai-compatible provider');
142
+ logger.debug(
143
+ "CONFIG",
144
+ "Validation failed: base_url missing for openai-compatible provider"
145
+ );
102
146
  }
103
147
 
104
- if (config.aiProvider.type === 'anthropic-compatible') {
148
+ if (config.aiProvider.type === "anthropic-compatible") {
105
149
  if (!config.aiProvider.baseURL) {
106
- const errorMsg = 'base_url is required for anthropic-compatible providers';
150
+ const errorMsg =
151
+ "base_url is required for anthropic-compatible providers";
107
152
  errors.push(errorMsg);
108
- logger.debug('CONFIG', 'Validation failed: base_url missing for anthropic-compatible provider');
153
+ logger.debug(
154
+ "CONFIG",
155
+ "Validation failed: base_url missing for anthropic-compatible provider"
156
+ );
109
157
  }
110
158
  if (!config.aiProvider.model) {
111
- const errorMsg = 'model is required for anthropic-compatible providers';
159
+ const errorMsg = "model is required for anthropic-compatible providers";
112
160
  errors.push(errorMsg);
113
- logger.debug('CONFIG', 'Validation failed: model missing for anthropic-compatible provider');
161
+ logger.debug(
162
+ "CONFIG",
163
+ "Validation failed: model missing for anthropic-compatible provider"
164
+ );
114
165
  }
115
166
  }
116
167
  }
117
168
 
118
169
  function validateReposPath(config: LibrarianConfig, errors: string[]): void {
119
170
  if (!config.repos_path) {
120
- errors.push('repos_path is required in configuration');
171
+ errors.push("repos_path is required in configuration");
121
172
  }
122
173
  }
123
174
 
124
175
  function validateTechnologies(config: LibrarianConfig, errors: string[]): void {
125
- const hasTechnologies = config.technologies && Object.keys(config.technologies).length > 0;
176
+ const hasTechnologies =
177
+ config.technologies && Object.keys(config.technologies).length > 0;
126
178
  if (!hasTechnologies) {
127
- errors.push('No technologies defined in configuration');
179
+ errors.push("No technologies defined in configuration");
128
180
  // Continue to validate group names even if empty - this is intentional
129
181
  // to catch path traversal in group names if any are defined
130
182
  }
@@ -134,26 +186,38 @@ function validateTechnologies(config: LibrarianConfig, errors: string[]): void {
134
186
  }
135
187
 
136
188
  for (const [groupName, group] of Object.entries(config.technologies)) {
137
- if (groupName.includes('..')) {
189
+ if (groupName.includes("..")) {
138
190
  errors.push(`Group name "${groupName}" contains invalid path characters`);
139
- logger.debug('CONFIG', 'Validation failed: group name contains path traversal', { groupName });
191
+ logger.debug(
192
+ "CONFIG",
193
+ "Validation failed: group name contains path traversal",
194
+ { groupName }
195
+ );
140
196
  }
141
197
 
142
198
  for (const [techName, tech] of Object.entries(group)) {
143
199
  if (!tech.repo) {
144
200
  const errorMsg = `Technology "${techName}" in group "${groupName}" is missing required "repo" field`;
145
201
  errors.push(errorMsg);
146
- logger.debug('CONFIG', 'Validation failed: missing repo field', { techName, groupName });
202
+ logger.debug("CONFIG", "Validation failed: missing repo field", {
203
+ techName,
204
+ groupName,
205
+ });
147
206
  continue;
148
207
  }
149
208
 
150
- const isRemoteUrl = tech.repo.startsWith('http://') || tech.repo.startsWith('https://');
151
- const isFileUrl = tech.repo.startsWith('file://');
152
- const hasProtocol = tech.repo.includes('://');
209
+ const isRemoteUrl =
210
+ tech.repo.startsWith("http://") || tech.repo.startsWith("https://");
211
+ const isFileUrl = tech.repo.startsWith("file://");
212
+ const hasProtocol = tech.repo.includes("://");
153
213
  if (!(isRemoteUrl || isFileUrl) && hasProtocol) {
154
214
  const errorMsg = `Technology "${techName}" has invalid repo URL: ${tech.repo}. Must be http://, https://, file://, or a local path`;
155
215
  errors.push(errorMsg);
156
- logger.debug('CONFIG', 'Validation failed: invalid repo URL', { techName, groupName, repoUrl: tech.repo });
216
+ logger.debug("CONFIG", "Validation failed: invalid repo URL", {
217
+ techName,
218
+ groupName,
219
+ repoUrl: tech.repo,
220
+ });
157
221
  }
158
222
  }
159
223
  }
@@ -161,14 +225,16 @@ function validateTechnologies(config: LibrarianConfig, errors: string[]): void {
161
225
 
162
226
  function reportErrors(errors: string[]): void {
163
227
  if (errors.length > 0) {
164
- logger.error('CONFIG', 'Configuration validation failed', undefined, { errorCount: errors.length });
165
- console.error('Configuration validation failed:');
228
+ logger.error("CONFIG", "Configuration validation failed", undefined, {
229
+ errorCount: errors.length,
230
+ });
231
+ console.error("Configuration validation failed:");
166
232
  for (const err of errors) {
167
233
  console.error(` - ${err}`);
168
234
  }
169
235
  process.exit(1);
170
236
  } else {
171
- logger.info('CONFIG', 'Configuration validation passed');
237
+ logger.info("CONFIG", "Configuration validation passed");
172
238
  }
173
239
  }
174
240
 
@@ -176,7 +242,7 @@ function reportErrors(errors: string[]): void {
176
242
  * Validate the configuration and exit with error if invalid
177
243
  */
178
244
  function validateConfig(config: LibrarianConfig, envPath: string): void {
179
- logger.debug('CONFIG', 'Validating configuration');
245
+ logger.debug("CONFIG", "Validating configuration");
180
246
  const errors: string[] = [];
181
247
 
182
248
  validateApiKey(config, envPath, errors);
@@ -187,9 +253,11 @@ function validateConfig(config: LibrarianConfig, envPath: string): void {
187
253
  reportErrors(errors);
188
254
  }
189
255
 
190
- type TechnologiesType = z.infer<typeof ConfigSchema>['technologies'];
256
+ type TechnologiesType = z.infer<typeof ConfigSchema>["technologies"];
191
257
 
192
- function normalizeTechnologies(technologies: TechnologiesType): TechnologiesType {
258
+ function normalizeTechnologies(
259
+ technologies: TechnologiesType
260
+ ): TechnologiesType {
193
261
  if (!technologies) {
194
262
  return undefined;
195
263
  }
@@ -197,93 +265,127 @@ function normalizeTechnologies(technologies: TechnologiesType): TechnologiesType
197
265
  for (const group of Object.values(technologies ?? {})) {
198
266
  for (const tech of Object.values(group ?? {})) {
199
267
  if (!tech.repo && tech.name) {
200
- logger.debug('CONFIG', 'Normalizing technology: using "name" as "repo"', { name: tech.name });
268
+ logger.debug(
269
+ "CONFIG",
270
+ 'Normalizing technology: using "name" as "repo"',
271
+ { name: tech.name }
272
+ );
201
273
  tech.repo = tech.name;
202
274
  }
203
275
  }
204
276
  }
205
- logger.debug('CONFIG', 'Technologies normalized', { groupCount: Object.keys(technologies).length });
277
+ logger.debug("CONFIG", "Technologies normalized", {
278
+ groupCount: Object.keys(technologies).length,
279
+ });
206
280
  return technologies;
207
281
  }
208
282
 
209
- function buildAiProvider(validatedConfig: z.infer<typeof ConfigSchema>, envVars: Record<string, string>): LibrarianConfig['aiProvider'] {
283
+ function buildAiProvider(
284
+ validatedConfig: z.infer<typeof ConfigSchema>,
285
+ envVars: Record<string, string>
286
+ ): LibrarianConfig["aiProvider"] {
210
287
  if (validatedConfig.aiProvider) {
211
288
  const { type, model, baseURL } = validatedConfig.aiProvider;
212
289
  return {
213
290
  type,
214
- apiKey: validatedConfig.aiProvider.apiKey || envVars.LIBRARIAN_API_KEY || '',
291
+ apiKey:
292
+ validatedConfig.aiProvider.apiKey || envVars.LIBRARIAN_API_KEY || "",
215
293
  ...(model && { model }),
216
- ...(baseURL && { baseURL })
294
+ ...(baseURL && { baseURL }),
217
295
  };
218
296
  }
219
297
 
220
298
  if (validatedConfig.llm_provider) {
221
- logger.debug('CONFIG', 'Using README-style llm_* keys for AI provider');
299
+ logger.debug("CONFIG", "Using README-style llm_* keys for AI provider");
222
300
  return {
223
301
  type: validatedConfig.llm_provider,
224
- apiKey: envVars.LIBRARIAN_API_KEY || '',
302
+ apiKey: envVars.LIBRARIAN_API_KEY || "",
225
303
  ...(validatedConfig.llm_model && { model: validatedConfig.llm_model }),
226
- ...(validatedConfig.base_url && { baseURL: validatedConfig.base_url })
304
+ ...(validatedConfig.base_url && { baseURL: validatedConfig.base_url }),
227
305
  };
228
306
  }
229
307
 
230
- logger.error('CONFIG', 'AI provider is required in configuration');
231
- console.error('Configuration error: llm_provider (or aiProvider) is required in config.yaml');
308
+ logger.error("CONFIG", "AI provider is required in configuration");
309
+ console.error(
310
+ "Configuration error: llm_provider (or aiProvider) is required in config.yaml"
311
+ );
232
312
  process.exit(1);
233
313
  }
234
314
 
235
- export async function loadConfig(configPath?: string): Promise<LibrarianConfig> {
236
- const defaultPath = path.join(os.homedir(), '.config', 'librarian', 'config.yaml');
315
+ export async function loadConfig(
316
+ configPath?: string
317
+ ): Promise<LibrarianConfig> {
318
+ const defaultPath = path.join(
319
+ os.homedir(),
320
+ ".config",
321
+ "librarian",
322
+ "config.yaml"
323
+ );
237
324
  const actualPath = configPath ? expandTilde(configPath) : defaultPath;
238
325
 
239
- logger.info('CONFIG', 'Loading configuration', { configPath: actualPath.replace(os.homedir(), '~') });
326
+ logger.info("CONFIG", "Loading configuration", {
327
+ configPath: actualPath.replace(os.homedir(), "~"),
328
+ });
240
329
 
241
330
  const configDir = path.dirname(actualPath);
242
- const envPath = path.join(configDir, '.env');
331
+ const envPath = path.join(configDir, ".env");
243
332
  const envVars = await loadEnvFile(envPath);
244
333
 
245
334
  if (!(await Bun.file(actualPath).exists())) {
246
- logger.info('CONFIG', 'Config file not found, creating default config');
335
+ logger.info("CONFIG", "Config file not found, creating default config");
247
336
  try {
248
337
  await createDefaultConfig(actualPath);
249
- logger.info('CONFIG', 'Default config created successfully');
338
+ logger.info("CONFIG", "Default config created successfully");
250
339
  } catch (error) {
251
- logger.error('CONFIG', 'Failed to create default config', error instanceof Error ? error : new Error(String(error)), { actualPath: actualPath.replace(os.homedir(), '~') });
252
- console.error(`Failed to create default config at ${actualPath}: ${error instanceof Error ? error.message : String(error)}`);
340
+ logger.error(
341
+ "CONFIG",
342
+ "Failed to create default config",
343
+ error instanceof Error ? error : new Error(String(error)),
344
+ { actualPath: actualPath.replace(os.homedir(), "~") }
345
+ );
346
+ console.error(
347
+ `Failed to create default config at ${actualPath}: ${error instanceof Error ? error.message : String(error)}`
348
+ );
253
349
  process.exit(1);
254
350
  }
255
351
  }
256
352
 
257
353
  const configFileContent = await Bun.file(actualPath).text();
258
- logger.debug('CONFIG', 'Config file read successfully', { fileSize: configFileContent.length });
354
+ logger.debug("CONFIG", "Config file read successfully", {
355
+ fileSize: configFileContent.length,
356
+ });
259
357
 
260
358
  const parsedConfig = parse(configFileContent);
261
359
  const validatedConfig = ConfigSchema.parse(parsedConfig);
262
- logger.debug('CONFIG', 'Config schema validation passed');
360
+ logger.debug("CONFIG", "Config schema validation passed");
263
361
 
264
362
  const technologies = normalizeTechnologies(validatedConfig.technologies);
265
363
  const aiProvider = buildAiProvider(validatedConfig, envVars);
266
364
 
267
- logger.debug('CONFIG', 'API key source', {
365
+ logger.debug("CONFIG", "API key source", {
268
366
  fromEnv: !!envVars.LIBRARIAN_API_KEY,
269
- fromConfig: !!validatedConfig.aiProvider?.apiKey
367
+ fromConfig: !!validatedConfig.aiProvider?.apiKey,
270
368
  });
271
369
 
272
370
  const config = {
273
371
  ...validatedConfig,
274
372
  technologies: technologies || { default: {} },
275
373
  aiProvider,
276
- repos_path: validatedConfig.repos_path ? expandTilde(validatedConfig.repos_path) : undefined,
277
- workingDir: expandTilde(validatedConfig.workingDir)
374
+ repos_path: validatedConfig.repos_path
375
+ ? expandTilde(validatedConfig.repos_path)
376
+ : undefined,
377
+ workingDir: expandTilde(validatedConfig.workingDir),
278
378
  } as LibrarianConfig;
279
379
 
280
380
  validateConfig(config, envPath);
281
381
 
282
- logger.info('CONFIG', 'Config loaded successfully', {
382
+ logger.info("CONFIG", "Config loaded successfully", {
283
383
  aiProviderType: config.aiProvider.type,
284
384
  model: config.aiProvider.model,
285
385
  techGroupsCount: Object.keys(config.technologies || {}).length,
286
- reposPath: config.repos_path ? config.repos_path.replace(os.homedir(), '~') : 'workingDir'
386
+ reposPath: config.repos_path
387
+ ? config.repos_path.replace(os.homedir(), "~")
388
+ : "workingDir",
287
389
  });
288
390
 
289
391
  return config;
@@ -295,15 +397,15 @@ export async function createDefaultConfig(configPath: string): Promise<void> {
295
397
  aiProvider: {
296
398
  type: "openai-compatible",
297
399
  model: "grok-code",
298
- baseURL: "https://opencode.ai/zen/v1"
299
- }
400
+ baseURL: "https://opencode.ai/zen/v1",
401
+ },
300
402
  };
301
403
 
302
- // Ensure directory exists
303
- const configDir = path.dirname(configPath);
304
- await mkdir(configDir, { recursive: true });
404
+ // Ensure directory exists
405
+ const configDir = path.dirname(configPath);
406
+ await mkdir(configDir, { recursive: true });
305
407
 
306
- // Write YAML file
307
- const yamlString = stringify(defaultConfig);
308
- await Bun.write(configPath, yamlString);
309
- }
408
+ // Write YAML file
409
+ const yamlString = stringify(defaultConfig);
410
+ await Bun.write(configPath, yamlString);
411
+ }