@compilr-dev/agents 0.0.1 → 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.
@@ -0,0 +1,133 @@
1
+ /**
2
+ * OpenAI LLM Provider
3
+ *
4
+ * Implements LLMProvider interface for OpenAI models (GPT-4o, GPT-4o-mini, etc.)
5
+ * Extends OpenAICompatibleProvider for shared functionality.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * const provider = createOpenAIProvider({
10
+ * model: 'gpt-4o',
11
+ * apiKey: process.env.OPENAI_API_KEY
12
+ * });
13
+ * ```
14
+ *
15
+ * @remarks
16
+ * - Requires valid OpenAI API key
17
+ * - Default model is gpt-4o
18
+ * - Extended thinking is not supported (Claude-specific feature)
19
+ */
20
+ import { ProviderError } from '../errors.js';
21
+ import { OpenAICompatibleProvider } from './openai-compatible.js';
22
+ // Default configuration
23
+ const DEFAULT_MODEL = 'gpt-4o';
24
+ const DEFAULT_BASE_URL = 'https://api.openai.com';
25
+ /**
26
+ * OpenAI LLM Provider
27
+ *
28
+ * Provides streaming chat completion using OpenAI models.
29
+ * Supports GPT-4o, GPT-4o-mini, and other compatible models.
30
+ */
31
+ export class OpenAIProvider extends OpenAICompatibleProvider {
32
+ name = 'openai';
33
+ apiKey;
34
+ organization;
35
+ constructor(config = {}) {
36
+ const apiKey = config.apiKey ?? process.env.OPENAI_API_KEY;
37
+ if (!apiKey) {
38
+ throw new ProviderError('OpenAI API key not found. Set OPENAI_API_KEY environment variable or pass apiKey in config.', 'openai');
39
+ }
40
+ const baseConfig = {
41
+ baseUrl: config.baseUrl ?? DEFAULT_BASE_URL,
42
+ model: config.model ?? DEFAULT_MODEL,
43
+ maxTokens: config.maxTokens,
44
+ timeout: config.timeout,
45
+ };
46
+ super(baseConfig);
47
+ this.apiKey = apiKey;
48
+ this.organization = config.organization;
49
+ }
50
+ /**
51
+ * OpenAI authentication with Bearer token
52
+ */
53
+ getAuthHeaders() {
54
+ const headers = {
55
+ Authorization: `Bearer ${this.apiKey}`,
56
+ };
57
+ if (this.organization) {
58
+ headers['OpenAI-Organization'] = this.organization;
59
+ }
60
+ return headers;
61
+ }
62
+ /**
63
+ * OpenAI chat completions endpoint
64
+ */
65
+ getEndpointPath() {
66
+ return '/v1/chat/completions';
67
+ }
68
+ /**
69
+ * OpenAI uses standard body format (no provider-specific extensions needed)
70
+ */
71
+ buildProviderSpecificBody(_options) {
72
+ return {};
73
+ }
74
+ /**
75
+ * Map HTTP errors with OpenAI-specific messages
76
+ */
77
+ mapHttpError(status, body, _model) {
78
+ let message = `OpenAI error (${String(status)})`;
79
+ try {
80
+ const parsed = JSON.parse(body);
81
+ if (parsed.error?.message) {
82
+ message = parsed.error.message;
83
+ }
84
+ }
85
+ catch {
86
+ message = body || message;
87
+ }
88
+ switch (status) {
89
+ case 401:
90
+ return new ProviderError('Invalid OpenAI API key. Check your OPENAI_API_KEY.', 'openai', 401);
91
+ case 403:
92
+ return new ProviderError('Access denied. Check your OpenAI API key permissions.', 'openai', 403);
93
+ case 429:
94
+ return new ProviderError('OpenAI rate limit exceeded. Please wait and try again.', 'openai', 429);
95
+ case 500:
96
+ case 502:
97
+ case 503:
98
+ return new ProviderError('OpenAI service temporarily unavailable. Please try again later.', 'openai', status);
99
+ default:
100
+ return new ProviderError(message, 'openai', status);
101
+ }
102
+ }
103
+ /**
104
+ * Map connection errors with OpenAI-specific messages
105
+ */
106
+ mapConnectionError(_error) {
107
+ return new ProviderError('Failed to connect to OpenAI API. Check your internet connection.', 'openai');
108
+ }
109
+ }
110
+ /**
111
+ * Create an OpenAI provider instance
112
+ *
113
+ * @example
114
+ * ```typescript
115
+ * // Using environment variable (OPENAI_API_KEY)
116
+ * const provider = createOpenAIProvider();
117
+ *
118
+ * // With explicit API key
119
+ * const provider = createOpenAIProvider({ apiKey: 'sk-...' });
120
+ *
121
+ * // With custom model
122
+ * const provider = createOpenAIProvider({ model: 'gpt-4o-mini' });
123
+ *
124
+ * // With organization
125
+ * const provider = createOpenAIProvider({
126
+ * apiKey: 'sk-...',
127
+ * organization: 'org-...'
128
+ * });
129
+ * ```
130
+ */
131
+ export function createOpenAIProvider(config = {}) {
132
+ return new OpenAIProvider(config);
133
+ }
@@ -38,6 +38,12 @@ export interface GlobInput {
38
38
  * Return absolute paths instead of relative (default: false)
39
39
  */
40
40
  absolute?: boolean;
41
+ /**
42
+ * Directory names to exclude from search.
43
+ * Default excludes: node_modules, .git, dist, build, etc.
44
+ * Set to empty array [] to include all directories.
45
+ */
46
+ excludeDirs?: string[];
41
47
  }
42
48
  /**
43
49
  * Glob tool definition
@@ -59,4 +65,9 @@ export declare function createGlobTool(options?: {
59
65
  * Always include these patterns regardless of ignore
60
66
  */
61
67
  alwaysInclude?: string[];
68
+ /**
69
+ * Override default excluded directories.
70
+ * Defaults to: node_modules, .git, dist, build, etc.
71
+ */
72
+ excludeDirs?: string[];
62
73
  }): Tool<GlobInput>;
@@ -5,6 +5,29 @@ import { readdir } from 'node:fs/promises';
5
5
  import { join } from 'node:path';
6
6
  import { defineTool, createSuccessResult, createErrorResult } from '../define.js';
7
7
  import { isNodeError } from './utils.js';
8
+ /**
9
+ * Default directories to exclude from glob searches.
10
+ * These are typically large, generated, or not relevant for code search.
11
+ */
12
+ const DEFAULT_EXCLUDE_DIRS = [
13
+ 'node_modules',
14
+ '.git',
15
+ 'dist',
16
+ 'build',
17
+ '.next',
18
+ '.nuxt',
19
+ '.output',
20
+ 'coverage',
21
+ '.nyc_output',
22
+ '__pycache__',
23
+ '.pytest_cache',
24
+ 'venv',
25
+ '.venv',
26
+ 'target', // Rust/Java
27
+ 'vendor', // Go/PHP
28
+ '.cargo',
29
+ '.cache',
30
+ ];
8
31
  /**
9
32
  * Glob tool definition
10
33
  */
@@ -48,6 +71,11 @@ export const globTool = defineTool({
48
71
  type: 'boolean',
49
72
  description: 'Return absolute paths instead of relative',
50
73
  },
74
+ excludeDirs: {
75
+ type: 'array',
76
+ items: { type: 'string' },
77
+ description: 'Directory names to exclude (default: node_modules, .git, dist, build, etc.)',
78
+ },
51
79
  },
52
80
  required: ['pattern'],
53
81
  },
@@ -57,7 +85,7 @@ export const globTool = defineTool({
57
85
  * Execute the glob tool
58
86
  */
59
87
  async function executeGlob(input) {
60
- const { pattern, path: basePath = '.', includeHidden = false, onlyDirectories = false, onlyFiles = !onlyDirectories, maxDepth, maxResults = 1000, absolute = false, } = input;
88
+ const { pattern, path: basePath = '.', includeHidden = false, onlyDirectories = false, onlyFiles = !onlyDirectories, maxDepth, maxResults = 1000, absolute = false, excludeDirs = DEFAULT_EXCLUDE_DIRS, } = input;
61
89
  // Check if base path exists
62
90
  try {
63
91
  await readdir(basePath);
@@ -91,6 +119,7 @@ async function executeGlob(input) {
91
119
  maxResults,
92
120
  matches,
93
121
  basePath,
122
+ excludeDirs,
94
123
  });
95
124
  // Format output
96
125
  const results = absolute ? matches.map((m) => join(basePath, m)) : matches;
@@ -117,6 +146,7 @@ async function searchDirectory(basePath, relativePath, depth, options) {
117
146
  return;
118
147
  }
119
148
  const currentPath = relativePath ? join(basePath, relativePath) : basePath;
149
+ const excludeSet = new Set(options.excludeDirs);
120
150
  let entries;
121
151
  try {
122
152
  entries = await readdir(currentPath, { withFileTypes: true });
@@ -133,8 +163,12 @@ async function searchDirectory(basePath, relativePath, depth, options) {
133
163
  if (!options.includeHidden && entry.name.startsWith('.')) {
134
164
  continue;
135
165
  }
136
- const entryRelativePath = relativePath ? join(relativePath, entry.name) : entry.name;
137
166
  const isDir = entry.isDirectory();
167
+ // Skip excluded directories
168
+ if (isDir && excludeSet.has(entry.name)) {
169
+ continue;
170
+ }
171
+ const entryRelativePath = relativePath ? join(relativePath, entry.name) : entry.name;
138
172
  const isFile = entry.isFile();
139
173
  // Check if this entry matches the pattern
140
174
  if (options.pattern.test(entryRelativePath)) {
@@ -226,6 +260,11 @@ export function createGlobTool(options) {
226
260
  type: 'boolean',
227
261
  description: 'Return absolute paths instead of relative',
228
262
  },
263
+ excludeDirs: {
264
+ type: 'array',
265
+ items: { type: 'string' },
266
+ description: 'Directory names to exclude (default: node_modules, .git, dist, build, etc.)',
267
+ },
229
268
  },
230
269
  required: ['pattern'],
231
270
  },
@@ -235,9 +274,12 @@ export function createGlobTool(options) {
235
274
  if (options?.baseDir && !searchPath.startsWith('/')) {
236
275
  searchPath = join(options.baseDir, searchPath);
237
276
  }
277
+ // Apply custom excludeDirs from factory options if specified and input doesn't override
278
+ const excludeDirs = input.excludeDirs ?? options?.excludeDirs ?? DEFAULT_EXCLUDE_DIRS;
238
279
  return executeGlob({
239
280
  ...input,
240
281
  path: searchPath,
282
+ excludeDirs,
241
283
  });
242
284
  },
243
285
  });
@@ -50,6 +50,12 @@ export interface GrepInput {
50
50
  * Search recursively in directories (default: true)
51
51
  */
52
52
  recursive?: boolean;
53
+ /**
54
+ * Directory names to exclude from search.
55
+ * Default excludes: node_modules, .git, dist, build, etc.
56
+ * Set to empty array [] to include all directories.
57
+ */
58
+ excludeDirs?: string[];
53
59
  }
54
60
  /**
55
61
  * Grep tool definition
@@ -60,7 +66,6 @@ export declare const grepTool: Tool<GrepInput>;
60
66
  *
61
67
  * TODO: Future enhancements could include:
62
68
  * - maxFileSize: Skip files larger than specified size
63
- * - excludeDirs: Exclude directories by name (e.g., node_modules, .git)
64
69
  */
65
70
  export declare function createGrepTool(options?: {
66
71
  /**
@@ -71,4 +76,9 @@ export declare function createGrepTool(options?: {
71
76
  * Default file extensions to search
72
77
  */
73
78
  defaultExtensions?: string[];
79
+ /**
80
+ * Override default excluded directories.
81
+ * Defaults to: node_modules, .git, dist, build, etc.
82
+ */
83
+ excludeDirs?: string[];
74
84
  }): Tool<GrepInput>;
@@ -6,6 +6,29 @@ import { readdir, stat } from 'node:fs/promises';
6
6
  import { join, relative } from 'node:path';
7
7
  import { defineTool, createSuccessResult, createErrorResult } from '../define.js';
8
8
  import { isNodeError, isExtensionAllowed } from './utils.js';
9
+ /**
10
+ * Default directories to exclude from grep searches.
11
+ * These are typically large, generated, or not relevant for code search.
12
+ */
13
+ const DEFAULT_EXCLUDE_DIRS = [
14
+ 'node_modules',
15
+ '.git',
16
+ 'dist',
17
+ 'build',
18
+ '.next',
19
+ '.nuxt',
20
+ '.output',
21
+ 'coverage',
22
+ '.nyc_output',
23
+ '__pycache__',
24
+ '.pytest_cache',
25
+ 'venv',
26
+ '.venv',
27
+ 'target', // Rust/Java
28
+ 'vendor', // Go/PHP
29
+ '.cargo',
30
+ '.cache',
31
+ ];
9
32
  /**
10
33
  * Grep tool definition
11
34
  */
@@ -103,7 +126,7 @@ function safeRegexTest(regex, text) {
103
126
  */
104
127
  async function executeGrep(input) {
105
128
  try {
106
- const { pattern, path, ignoreCase = false, lineNumbers = true, before = 0, after = 0, filesOnly = false, includeHidden = false, extensions, maxMatches = 100, recursive = true, } = input;
129
+ const { pattern, path, ignoreCase = false, lineNumbers = true, before = 0, after = 0, filesOnly = false, includeHidden = false, extensions, maxMatches = 100, recursive = true, excludeDirs = DEFAULT_EXCLUDE_DIRS, } = input;
107
130
  // Check for potentially dangerous regex patterns
108
131
  if (isPotentiallyDangerousRegex(pattern)) {
109
132
  return createErrorResult(`Potentially dangerous regex pattern detected (ReDoS risk). ` +
@@ -128,6 +151,7 @@ async function executeGrep(input) {
128
151
  recursive,
129
152
  includeHidden,
130
153
  extensions,
154
+ excludeDirs,
131
155
  });
132
156
  }
133
157
  else {
@@ -189,6 +213,7 @@ async function executeGrep(input) {
189
213
  */
190
214
  async function collectFiles(dir, files, options) {
191
215
  const entries = await readdir(dir, { withFileTypes: true });
216
+ const excludeSet = new Set(options.excludeDirs ?? []);
192
217
  for (const entry of entries) {
193
218
  // Skip hidden files unless included
194
219
  if (!options.includeHidden && entry.name.startsWith('.')) {
@@ -205,6 +230,10 @@ async function collectFiles(dir, files, options) {
205
230
  files.push(fullPath);
206
231
  }
207
232
  else if (entry.isDirectory() && options.recursive) {
233
+ // Skip excluded directories
234
+ if (excludeSet.has(entry.name)) {
235
+ continue;
236
+ }
208
237
  await collectFiles(fullPath, files, options);
209
238
  }
210
239
  }
@@ -286,7 +315,6 @@ function formatMatches(matches, options) {
286
315
  *
287
316
  * TODO: Future enhancements could include:
288
317
  * - maxFileSize: Skip files larger than specified size
289
- * - excludeDirs: Exclude directories by name (e.g., node_modules, .git)
290
318
  */
291
319
  export function createGrepTool(options) {
292
320
  return defineTool({
@@ -342,6 +370,11 @@ export function createGrepTool(options) {
342
370
  type: 'boolean',
343
371
  description: 'Search recursively in directories (default: true)',
344
372
  },
373
+ excludeDirs: {
374
+ type: 'array',
375
+ items: { type: 'string' },
376
+ description: 'Directory names to exclude (default: node_modules, .git, dist, build, etc.)',
377
+ },
345
378
  },
346
379
  required: ['pattern', 'path'],
347
380
  },
@@ -353,10 +386,13 @@ export function createGrepTool(options) {
353
386
  }
354
387
  // Apply default extensions if none specified
355
388
  const extensions = input.extensions ?? options?.defaultExtensions;
389
+ // Apply custom excludeDirs from factory options if specified and input doesn't override
390
+ const excludeDirs = input.excludeDirs ?? options?.excludeDirs ?? DEFAULT_EXCLUDE_DIRS;
356
391
  return executeGrep({
357
392
  ...input,
358
393
  path: searchPath,
359
394
  extensions,
395
+ excludeDirs,
360
396
  });
361
397
  },
362
398
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compilr-dev/agents",
3
- "version": "0.0.1",
3
+ "version": "0.1.0",
4
4
  "description": "Lightweight multi-LLM agent library for building CLI AI assistants",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -64,7 +64,7 @@
64
64
  }
65
65
  },
66
66
  "devDependencies": {
67
- "@anthropic-ai/sdk": "^0.30.0",
67
+ "@anthropic-ai/sdk": "^0.30.1",
68
68
  "@eslint/js": "^9.39.1",
69
69
  "@modelcontextprotocol/sdk": "^1.23.0",
70
70
  "@types/node": "^24.10.1",