@smythos/sre 1.7.20 → 1.7.40

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 (54) hide show
  1. package/dist/index.js +114 -76
  2. package/dist/index.js.map +1 -1
  3. package/dist/types/Components/DataSourceIndexer.class.d.ts +4 -12
  4. package/dist/types/Components/GenAILLM.class.d.ts +5 -5
  5. package/dist/types/Components/index.d.ts +3 -3
  6. package/dist/types/index.d.ts +3 -3
  7. package/dist/types/subsystems/IO/VectorDB.service/connectors/MilvusVectorDB.class.d.ts +1 -0
  8. package/dist/types/subsystems/IO/VectorDB.service/connectors/PineconeVectorDB.class.d.ts +11 -4
  9. package/dist/types/subsystems/IO/VectorDB.service/embed/index.d.ts +5 -0
  10. package/dist/types/subsystems/LLMManager/ModelsProvider.service/connectors/JSONModelsProvider.class.d.ts +35 -0
  11. package/dist/types/subsystems/Security/Account.service/AccountConnector.d.ts +2 -2
  12. package/dist/types/subsystems/Security/Vault.service/connectors/SecretsManager.class.d.ts +2 -3
  13. package/dist/types/types/VectorDB.types.d.ts +4 -0
  14. package/dist/types/utils/string.utils.d.ts +1 -0
  15. package/package.json +3 -3
  16. package/src/Components/APIEndpoint.class.ts +1 -6
  17. package/src/Components/Component.class.ts +14 -1
  18. package/src/Components/DataSourceIndexer.class.ts +148 -34
  19. package/src/Components/GenAILLM.class.ts +21 -11
  20. package/src/Components/RAG/DataSourceCleaner.class.ts +178 -0
  21. package/src/Components/RAG/DataSourceComponent.class.ts +111 -0
  22. package/src/Components/RAG/DataSourceIndexer.class.ts +254 -0
  23. package/src/Components/{DataSourceLookup.class.ts → RAG/DataSourceLookup.class.ts} +92 -3
  24. package/src/Components/ServerlessCode.class.ts +1 -4
  25. package/src/Components/index.ts +3 -3
  26. package/src/helpers/S3Cache.helper.ts +2 -1
  27. package/src/index.ts +212 -212
  28. package/src/index.ts.bak +212 -212
  29. package/src/subsystems/IO/NKV.service/connectors/NKVRedis.class.ts +3 -1
  30. package/src/subsystems/IO/VectorDB.service/connectors/MilvusVectorDB.class.ts +145 -19
  31. package/src/subsystems/IO/VectorDB.service/connectors/PineconeVectorDB.class.ts +56 -22
  32. package/src/subsystems/IO/VectorDB.service/embed/GoogleEmbedding.ts +1 -0
  33. package/src/subsystems/IO/VectorDB.service/embed/OpenAIEmbedding.ts +2 -1
  34. package/src/subsystems/IO/VectorDB.service/embed/index.ts +18 -0
  35. package/src/subsystems/LLMManager/LLM.service/connectors/Anthropic.class.ts +35 -10
  36. package/src/subsystems/LLMManager/LLM.service/connectors/Bedrock.class.ts +12 -4
  37. package/src/subsystems/LLMManager/LLM.service/connectors/Echo.class.ts +4 -4
  38. package/src/subsystems/LLMManager/LLM.service/connectors/GoogleAI.class.ts +13 -4
  39. package/src/subsystems/LLMManager/LLM.service/connectors/Groq.class.ts +17 -5
  40. package/src/subsystems/LLMManager/LLM.service/connectors/Ollama.class.ts +18 -3
  41. package/src/subsystems/LLMManager/LLM.service/connectors/Perplexity.class.ts +14 -5
  42. package/src/subsystems/LLMManager/LLM.service/connectors/VertexAI.class.ts +6 -4
  43. package/src/subsystems/LLMManager/LLM.service/connectors/openai/apiInterfaces/ChatCompletionsApiInterface.ts +5 -5
  44. package/src/subsystems/LLMManager/LLM.service/connectors/openai/apiInterfaces/ResponsesApiInterface.ts +8 -3
  45. package/src/subsystems/LLMManager/LLM.service/connectors/xAI.class.ts +9 -8
  46. package/src/subsystems/LLMManager/ModelsProvider.service/connectors/JSONModelsProvider.class.ts +92 -1
  47. package/src/subsystems/ObservabilityManager/Telemetry.service/connectors/OTel/OTel.class.ts +32 -6
  48. package/src/subsystems/Security/Account.service/AccountConnector.ts +3 -3
  49. package/src/subsystems/Security/Vault.service/connectors/SecretsManager.class.ts +8 -63
  50. package/src/types/VectorDB.types.ts +4 -0
  51. package/src/utils/array.utils.ts +11 -0
  52. package/src/utils/base64.utils.ts +1 -1
  53. package/src/utils/string.utils.ts +3 -192
  54. package/src/Components/DataSourceCleaner.class.ts +0 -92
@@ -1,7 +1,7 @@
1
1
  import { JSONContent } from '@sre/helpers/JsonContent.helper';
2
2
  import { LLMConnector } from '../LLMConnector';
3
3
  import EventEmitter from 'events';
4
- import { APIKeySource, ILLMRequestFuncParams, TLLMChatResponse, TLLMPreparedParams } from '@sre/types/LLM.types';
4
+ import { APIKeySource, ILLMRequestFuncParams, TLLMChatResponse, TLLMEvent, TLLMPreparedParams } from '@sre/types/LLM.types';
5
5
  import { Logger } from '@sre/helpers/Log.helper';
6
6
  import { delay } from '@sre/utils/index';
7
7
  import { hookAsync } from '@sre/Core/HookService';
@@ -54,13 +54,13 @@ export class EchoConnector extends LLMConnector {
54
54
  const isLastChunk = i === chunks.length - 1;
55
55
  // Add space between chunks except for the last one to avoid trailing space in file URLs
56
56
  const delta = { content: chunks[i] + (isLastChunk ? '' : ' ') };
57
- emitter.emit('data', delta);
58
- emitter.emit('content', delta.content);
57
+ emitter.emit(TLLMEvent.Data, delta);
58
+ emitter.emit(TLLMEvent.Content, delta.content);
59
59
  }
60
60
 
61
61
  // Emit end event after all chunks are processed
62
62
  setTimeout(() => {
63
- emitter.emit('end', [], []); // Empty arrays for toolsData and usage_data
63
+ emitter.emit(TLLMEvent.End, [], [], 'stop'); // Empty arrays for toolsData and usage_data, with finishReason
64
64
  }, 100);
65
65
  })();
66
66
 
@@ -186,9 +186,11 @@ export class GoogleAIConnector extends LLMConnector {
186
186
  (async () => {
187
187
  try {
188
188
  for await (const chunk of stream) {
189
+ emitter.emit(TLLMEvent.Data, chunk);
190
+
189
191
  const chunkText = chunk.text ?? '';
190
192
  if (chunkText) {
191
- emitter.emit('content', chunkText);
193
+ emitter.emit(TLLMEvent.Content, chunkText);
192
194
  }
193
195
 
194
196
  const toolCalls = chunk.candidates?.[0]?.content?.parts?.filter((part) => part.functionCall);
@@ -213,21 +215,28 @@ export class GoogleAIConnector extends LLMConnector {
213
215
  }
214
216
  }
215
217
 
218
+ const finishReason = 'stop'; // GoogleAI doesn't provide finishReason in streaming
219
+ const reportedUsage: any[] = [];
220
+
216
221
  if (usage) {
217
- this.reportUsage(usage, {
222
+ const reported = this.reportUsage(usage, {
218
223
  modelEntryName: context.modelEntryName,
219
224
  keySource: context.isUserKey ? APIKeySource.User : APIKeySource.Smyth,
220
225
  agentId: context.agentId,
221
226
  teamId: context.teamId,
222
227
  });
228
+ reportedUsage.push(reported);
223
229
  }
224
230
 
231
+ // Note: GoogleAI stream doesn't provide explicit finish reasons
232
+ // If we had a non-stop finish reason, we would emit Interrupted here
233
+
225
234
  setTimeout(() => {
226
- emitter.emit('end', toolsData);
235
+ emitter.emit(TLLMEvent.End, toolsData, reportedUsage, finishReason);
227
236
  }, 100);
228
237
  } catch (error) {
229
238
  logger.error(`streamRequest ${this.name}`, error, acRequest.candidate);
230
- emitter.emit('error', error);
239
+ emitter.emit(TLLMEvent.Error, error);
231
240
  }
232
241
  })();
233
242
 
@@ -108,6 +108,7 @@ export class GroqConnector extends LLMConnector {
108
108
  const stream = await groq.chat.completions.create({ ...body, stream: true, stream_options: { include_usage: true } });
109
109
 
110
110
  let toolsData: ToolData[] = [];
111
+ let finishReason = 'stop';
111
112
 
112
113
  (async () => {
113
114
  for await (const chunk of stream as any) {
@@ -117,10 +118,10 @@ export class GroqConnector extends LLMConnector {
117
118
  if (usage) {
118
119
  usage_data.push(usage);
119
120
  }
120
- emitter.emit('data', delta);
121
+ emitter.emit(TLLMEvent.Data, delta);
121
122
 
122
123
  if (delta?.content) {
123
- emitter.emit('content', delta.content);
124
+ emitter.emit(TLLMEvent.Content, delta.content);
124
125
  }
125
126
 
126
127
  if (delta?.tool_calls) {
@@ -139,24 +140,35 @@ export class GroqConnector extends LLMConnector {
139
140
  }
140
141
  });
141
142
  }
143
+
144
+ // Capture finish reason
145
+ if (chunk.choices[0]?.finish_reason) {
146
+ finishReason = chunk.choices[0].finish_reason;
147
+ }
142
148
  }
143
149
 
144
150
  if (toolsData.length > 0) {
145
151
  emitter.emit(TLLMEvent.ToolInfo, toolsData);
146
152
  }
147
153
 
154
+ const reportedUsage: any[] = [];
148
155
  usage_data.forEach((usage) => {
149
- // probably we can acc them and send them as one event
150
- this.reportUsage(usage, {
156
+ const reported = this.reportUsage(usage, {
151
157
  modelEntryName: context.modelEntryName,
152
158
  keySource: context.isUserKey ? APIKeySource.User : APIKeySource.Smyth,
153
159
  agentId: context.agentId,
154
160
  teamId: context.teamId,
155
161
  });
162
+ reportedUsage.push(reported);
156
163
  });
157
164
 
165
+ // Emit interrupted event if finishReason is not 'stop'
166
+ if (finishReason !== 'stop') {
167
+ emitter.emit(TLLMEvent.Interrupted, finishReason);
168
+ }
169
+
158
170
  setTimeout(() => {
159
- emitter.emit('end', toolsData);
171
+ emitter.emit(TLLMEvent.End, toolsData, reportedUsage, finishReason);
160
172
  }, 100);
161
173
  })();
162
174
 
@@ -138,14 +138,17 @@ export class OllamaConnector extends LLMConnector {
138
138
 
139
139
  let toolsData: ToolData[] = [];
140
140
  let fullContent = '';
141
+ let finishReason = 'stop';
141
142
 
142
143
  (async () => {
143
144
  for await (const chunk of stream) {
145
+ emitter.emit(TLLMEvent.Data, chunk);
146
+
144
147
  // Emit content deltas
145
148
  if (chunk.message?.content) {
146
149
  const content = chunk.message.content;
147
150
  fullContent += content;
148
- emitter.emit('content', content);
151
+ emitter.emit(TLLMEvent.Content, content);
149
152
  }
150
153
 
151
154
  // Handle tool calls accumulation
@@ -181,6 +184,11 @@ export class OllamaConnector extends LLMConnector {
181
184
  };
182
185
  usage_data.push(usage);
183
186
  }
187
+
188
+ // Capture finish reason from Ollama's done_reason
189
+ if (chunk.done_reason) {
190
+ finishReason = chunk.done_reason;
191
+ }
184
192
  }
185
193
 
186
194
  // Emit tool info if tools were requested
@@ -189,18 +197,25 @@ export class OllamaConnector extends LLMConnector {
189
197
  }
190
198
 
191
199
  // Report usage
200
+ const reportedUsage: any[] = [];
192
201
  usage_data.forEach((usage) => {
193
- this.reportUsage(usage, {
202
+ const reported = this.reportUsage(usage, {
194
203
  modelEntryName: context.modelEntryName,
195
204
  keySource: context.isUserKey ? APIKeySource.User : APIKeySource.Smyth,
196
205
  agentId: context.agentId,
197
206
  teamId: context.teamId,
198
207
  });
208
+ reportedUsage.push(reported);
199
209
  });
200
210
 
211
+ // Emit interrupted event if finishReason is not 'stop'
212
+ if (finishReason !== 'stop') {
213
+ emitter.emit(TLLMEvent.Interrupted, finishReason);
214
+ }
215
+
201
216
  // Final end event
202
217
  setTimeout(() => {
203
- emitter.emit('end', toolsData);
218
+ emitter.emit(TLLMEvent.End, toolsData, reportedUsage, finishReason);
204
219
  }, 100);
205
220
  })();
206
221
 
@@ -14,6 +14,7 @@ import {
14
14
  TLLMChatResponse,
15
15
  ILLMRequestContext,
16
16
  TLLMPreparedParams,
17
+ TLLMEvent,
17
18
  } from '@sre/types/LLM.types';
18
19
  import { LLMHelper } from '@sre/LLMManager/LLM.helper';
19
20
 
@@ -97,6 +98,8 @@ export class PerplexityConnector extends LLMConnector {
97
98
  //fallback to chatRequest
98
99
  const emitter = new EventEmitter();
99
100
 
101
+ // TODO: need to implement proper streaming for Perplexity
102
+
100
103
  setTimeout(() => {
101
104
  try {
102
105
  logger.debug(`streamRequest ${this.name}`, acRequest.candidate);
@@ -105,17 +108,23 @@ export class PerplexityConnector extends LLMConnector {
105
108
  const finishReason = respose.finishReason;
106
109
  const usage = respose.usage;
107
110
 
108
- emitter.emit('interrupted', finishReason);
109
- emitter.emit('content', respose.content);
110
- emitter.emit('end', undefined, usage, finishReason);
111
+ emitter.emit(TLLMEvent.Data, respose);
112
+ emitter.emit(TLLMEvent.Content, respose.content);
113
+
114
+ // Only emit Interrupted if finishReason is not 'stop'
115
+ if (finishReason !== 'stop') {
116
+ emitter.emit(TLLMEvent.Interrupted, finishReason);
117
+ }
118
+
119
+ emitter.emit(TLLMEvent.End, [], [usage], finishReason);
111
120
  })
112
121
  .catch((error) => {
113
- emitter.emit('error', error.message || error.toString());
122
+ emitter.emit(TLLMEvent.Error, error.message || error.toString());
114
123
  });
115
124
  //emitter.emit('finishReason', respose.finishReason);
116
125
  } catch (error) {
117
126
  logger.error(`streamRequest ${this.name}`, error, acRequest.candidate);
118
- emitter.emit('error', error.message || error.toString());
127
+ emitter.emit(TLLMEvent.Error, error.message || error.toString());
119
128
  }
120
129
  }, 100);
121
130
 
@@ -138,12 +138,14 @@ export class VertexAIConnector extends LLMConnector {
138
138
  for await (const chunk of streamResult.stream) {
139
139
  const chunkText = chunk.candidates?.[0]?.content?.parts?.[0]?.text || '';
140
140
  if (chunkText) {
141
- emitter.emit('content', chunkText);
141
+ emitter.emit(TLLMEvent.Content, chunkText);
142
142
  }
143
143
  }
144
144
 
145
145
  const aggregatedResponse = await streamResult.response;
146
146
 
147
+ emitter.emit(TLLMEvent.Data, aggregatedResponse);
148
+
147
149
  // Check for function calls in the final response (like Anthropic does)
148
150
  const functionCalls = aggregatedResponse.candidates?.[0]?.content?.parts?.filter((part) => part.functionCall);
149
151
  if (functionCalls && functionCalls.length > 0) {
@@ -176,15 +178,15 @@ export class VertexAIConnector extends LLMConnector {
176
178
  const finishReason = (aggregatedResponse.candidates?.[0]?.finishReason || 'stop').toLowerCase();
177
179
 
178
180
  if (finishReason !== 'stop') {
179
- emitter.emit('interrupted', finishReason);
181
+ emitter.emit(TLLMEvent.Interrupted, finishReason);
180
182
  }
181
183
 
182
184
  setTimeout(() => {
183
- emitter.emit('end', toolsData, usageData, finishReason);
185
+ emitter.emit(TLLMEvent.End, toolsData, usageData, finishReason);
184
186
  }, 100);
185
187
  } catch (error) {
186
188
  logger.error(`streamRequest ${this.name}`, error, acRequest.candidate);
187
- emitter.emit('error', error);
189
+ emitter.emit(TLLMEvent.Error, error);
188
190
  }
189
191
  }, 100);
190
192
 
@@ -73,7 +73,7 @@ export class ChatCompletionsApiInterface extends OpenAIApiInterface {
73
73
  // Step 3: Emit final events
74
74
  this.emitFinalEvents(emitter, finalToolsData, reportedUsage, finishReason);
75
75
  } catch (error) {
76
- emitter.emit('error', error);
76
+ emitter.emit(TLLMEvent.Error, error);
77
77
  }
78
78
  })();
79
79
 
@@ -267,11 +267,11 @@ export class ChatCompletionsApiInterface extends OpenAIApiInterface {
267
267
  }
268
268
 
269
269
  // Emit data event for delta
270
- emitter.emit('data', delta);
270
+ emitter.emit(TLLMEvent.Data, delta);
271
271
 
272
272
  // Handle content deltas
273
273
  if (!delta?.tool_calls && delta?.content) {
274
- emitter.emit('content', delta?.content, delta?.role);
274
+ emitter.emit(TLLMEvent.Content, delta?.content, delta?.role);
275
275
  }
276
276
 
277
277
  // Handle tool calls
@@ -350,12 +350,12 @@ export class ChatCompletionsApiInterface extends OpenAIApiInterface {
350
350
 
351
351
  // Emit interrupted event if finishReason is not 'stop'
352
352
  if (finishReason !== 'stop') {
353
- emitter.emit('interrupted', finishReason);
353
+ emitter.emit(TLLMEvent.Interrupted, finishReason);
354
354
  }
355
355
 
356
356
  // Emit end event with setImmediate to ensure proper event ordering
357
357
  setImmediate(() => {
358
- emitter.emit('end', toolsData, reportedUsage, finishReason);
358
+ emitter.emit(TLLMEvent.End, toolsData, reportedUsage, finishReason);
359
359
  });
360
360
  }
361
361
 
@@ -119,7 +119,7 @@ export class ResponsesApiInterface extends OpenAIApiInterface {
119
119
  // Step 3: Emit final events
120
120
  this.emitFinalEvents(emitter, finalToolsData, reportedUsage, finishReason);
121
121
  } catch (error) {
122
- emitter.emit('error', error);
122
+ emitter.emit(TLLMEvent.Error, error);
123
123
  }
124
124
  })();
125
125
 
@@ -362,8 +362,10 @@ export class ResponsesApiInterface extends OpenAIApiInterface {
362
362
  role: 'assistant',
363
363
  content: part.delta,
364
364
  };
365
- emitter.emit('data', deltaMsg);
366
- emitter.emit('content', part.delta, 'assistant');
365
+
366
+ // TODO: we have inconsistency for data event with chat completions API, we need to check and fix it
367
+ emitter.emit(TLLMEvent.Data, deltaMsg);
368
+ emitter.emit(TLLMEvent.Content, part.delta, 'assistant');
367
369
  }
368
370
  } catch (error) {
369
371
  console.warn('Error handling output text delta:', error);
@@ -412,6 +414,7 @@ export class ResponsesApiInterface extends OpenAIApiInterface {
412
414
  }
413
415
 
414
416
  if (addingNew) {
417
+ // TODO: Check whether this event is being used.
415
418
  emitter.emit('tool_call_started', {
416
419
  id: callId,
417
420
  name: functionName || '',
@@ -458,6 +461,7 @@ export class ResponsesApiInterface extends OpenAIApiInterface {
458
461
  }
459
462
 
460
463
  const entry = existingIndex === -1 ? updated[finalIndex] : updated[finalIndex];
464
+ // TODO: Check whether this event is being used.
461
465
  emitter.emit('tool_call_progress', {
462
466
  id: entry.callId || itemId,
463
467
  name: entry.name,
@@ -489,6 +493,7 @@ export class ResponsesApiInterface extends OpenAIApiInterface {
489
493
  const updated = toolsData.map((t, idx) => (idx === toolIndex ? { ...t, arguments: finalArguments } : t));
490
494
 
491
495
  const updatedEntry = updated[toolIndex];
496
+ // TODO: Check whether this event is being used.
492
497
  emitter.emit('tool_call_completed', {
493
498
  id: updatedEntry.callId || itemId,
494
499
  name: updatedEntry.name,
@@ -14,6 +14,7 @@ import {
14
14
  ILLMRequestFuncParams,
15
15
  TLLMChatResponse,
16
16
  ILLMRequestContext,
17
+ TLLMEvent,
17
18
  } from '@sre/types/LLM.types';
18
19
  import { LLMHelper } from '@sre/LLMManager/LLM.helper';
19
20
 
@@ -202,10 +203,10 @@ export class xAIConnector extends LLMConnector {
202
203
  }
203
204
 
204
205
  if (delta) {
205
- emitter.emit('data', delta);
206
+ emitter.emit(TLLMEvent.Data, delta);
206
207
 
207
208
  if (delta.content) {
208
- emitter.emit('content', delta.content, delta.role);
209
+ emitter.emit(TLLMEvent.Content, delta.content, delta.role);
209
210
  }
210
211
 
211
212
  if (delta.tool_calls) {
@@ -238,11 +239,11 @@ export class xAIConnector extends LLMConnector {
238
239
  if (citations && citations.length > 0) {
239
240
  const citationsText = '\n\n**Sources:**\n' + citations.map((url, index) => `${index + 1}. ${url}`).join('\n');
240
241
 
241
- emitter.emit('content', citationsText, 'assistant');
242
+ emitter.emit(TLLMEvent.Content, citationsText, 'assistant');
242
243
  }
243
244
 
244
245
  if (toolsData.length > 0) {
245
- emitter.emit('toolInfo', toolsData);
246
+ emitter.emit(TLLMEvent.ToolInfo, toolsData);
246
247
  }
247
248
 
248
249
  // Report usage if available
@@ -257,20 +258,20 @@ export class xAIConnector extends LLMConnector {
257
258
  }
258
259
 
259
260
  if (finishReason !== 'stop') {
260
- emitter.emit('interrupted', finishReason);
261
+ emitter.emit(TLLMEvent.Interrupted, finishReason);
261
262
  }
262
263
 
263
264
  setTimeout(() => {
264
- emitter.emit('end', toolsData, reportedUsage, finishReason);
265
+ emitter.emit(TLLMEvent.End, toolsData, reportedUsage, finishReason);
265
266
  }, 100);
266
267
  });
267
268
 
268
269
  response.data.on('error', (error) => {
269
- emitter.emit('error', error);
270
+ emitter.emit(TLLMEvent.Error, error);
270
271
  });
271
272
  } catch (error) {
272
273
  logger.error(`streamRequest ${this.name}`, error, acRequest.candidate);
273
- emitter.emit('error', error);
274
+ emitter.emit(TLLMEvent.Error, error);
274
275
  }
275
276
 
276
277
  return emitter;
@@ -214,6 +214,93 @@ export class JSONModelsProvider extends ModelsProviderConnector {
214
214
  return this.isValidSingleModel(data);
215
215
  }
216
216
 
217
+ /**
218
+ * Determines whether a file path should be ignored by the directory watcher.
219
+ *
220
+ * This method implements a sophisticated filtering strategy for dot-segment paths
221
+ * (paths containing directories that start with a dot, like .git, .env, .cache).
222
+ *
223
+ * **Filtering Strategy:**
224
+ * 1. Paths WITHOUT dot segments: Never ignored
225
+ * 2. Paths WITH dot segments:
226
+ * - If SMYTH_PATH is not configured: All ignored
227
+ * - If SMYTH_PATH is configured:
228
+ * - Allow the watched directory even if SMYTH_PATH contains dot-segments
229
+ * (e.g., /home/user/.smyth/models/OpenAI/default.json is allowed)
230
+ * - Ignore dot-segments INSIDE the models directory
231
+ * (e.g., /home/user/.smyth/models/.hidden/model.json is ignored)
232
+ * - Paths outside watched directory: Ignored
233
+ *
234
+ * @param filePath - The file path to check
235
+ * @param watchedDir - The absolute path of the directory being watched (models folder)
236
+ * @param smythPath - The resolved SMYTH_PATH, or null if not configured
237
+ * @returns true if the path should be ignored, false if it should be watched
238
+ *
239
+ * @example
240
+ * ```typescript
241
+ * // Path without dot-segment (allowed)
242
+ * shouldIgnorePath('/models/OpenAI/default.json', '/models', '/home/.smyth') // => false
243
+ *
244
+ * // Dot-segment inside models directory (ignored)
245
+ * shouldIgnorePath('/models/.git/config', '/models', '/home/.smyth') // => true
246
+ *
247
+ * // Dot-segment in parent path only (allowed)
248
+ * shouldIgnorePath('/home/.smyth/models/OpenAI/default.json', '/home/.smyth/models', '/home/.smyth') // => false
249
+ * ```
250
+ */
251
+ private shouldIgnorePath(filePath: string, watchedDir: string, smythPath: string | null): boolean {
252
+ // Check if the file path contains a dot-segment (e.g., /.git/, /.env/, /.cache/)
253
+ // Regex explanation: [\\/]\. matches a path separator (/ or \) followed by a dot
254
+ const hasDotSegment = /[\\/]\./.test(filePath);
255
+
256
+ // CASE 1: If there is NO dot-segment at all, we never ignore this path
257
+ // Examples: /models/OpenAI/default.json, /models/Anthropic/claude.json
258
+ if (!hasDotSegment) {
259
+ return false;
260
+ }
261
+
262
+ // CASE 2: If there IS a dot-segment and SMYTH_PATH is not configured,
263
+ // we ignore all such paths to prevent watching system/hidden files
264
+ // Examples: /.git/config, /node_modules/.cache/file.json
265
+ if (hasDotSegment && !smythPath) {
266
+ return true;
267
+ }
268
+
269
+ // Resolve the file path to an absolute path for accurate comparison
270
+ // This ensures we can reliably compare against the watched directory path
271
+ const resolvedPath = path.resolve(filePath);
272
+
273
+ // Check if the resolved path is inside the watched directory (models folder)
274
+ // This handles two cases:
275
+ // 1. The path exactly matches the watched directory
276
+ // 2. The path is a child of the watched directory (starts with watchedDir + separator)
277
+ const isInsideWatchedDir = resolvedPath === watchedDir || resolvedPath.startsWith(watchedDir + path.sep);
278
+
279
+ // CASE 3: If the path is outside the watched directory, ignore it
280
+ // This prevents watching unrelated files that happen to have dot-segments
281
+ if (!isInsideWatchedDir) {
282
+ return true;
283
+ }
284
+
285
+ // CASE 4: Path is inside the watched directory
286
+ // Now we need to determine if the dot-segment is in the models directory itself
287
+ // or if it is part of the parent path (e.g., SMYTH_PATH containing .smyth)
288
+
289
+ // Get the relative path from the watched directory to determine where the dot-segment is
290
+ const relativePath = path.relative(watchedDir, resolvedPath);
291
+
292
+ // Check if the dot-segment appears in the relative portion (inside models directory)
293
+ // Regex explanation: (^|[\\/])\. matches a dot at the start OR after a path separator
294
+ // Examples that match: '.git/config', 'subdir/.hidden/file.json'
295
+ const hasDotSegmentInsideWatchedDir = /(^|[\\/])\./.test(relativePath);
296
+
297
+ // FINAL DECISION:
298
+ // - If dot-segment is INSIDE the models directory (e.g., models/.git/config), IGNORE it (return true)
299
+ // - If dot-segment is OUTSIDE the models directory (e.g., /home/user/.smyth/models/OpenAI/default.json), ALLOW it (return false)
300
+ // This allows SMYTH_PATH to contain dot-segments while preventing dot-segments within the models folder
301
+ return hasDotSegmentInsideWatchedDir;
302
+ }
303
+
217
304
  private initDirWatcher(dir) {
218
305
  const stats = fsSync.statSync(dir);
219
306
 
@@ -257,8 +344,12 @@ export class JSONModelsProvider extends ModelsProviderConnector {
257
344
  maxWait: 5000,
258
345
  });
259
346
 
347
+ const smythPath = process.env.SMYTH_PATH ? path.resolve(process.env.SMYTH_PATH) : null;
348
+ const watchedDir = path.resolve(dir);
349
+
260
350
  const watcher = chokidar.watch(dir, {
261
- ignored: /(^|[\/\\])\../, // ignore dotfiles
351
+ // Use the extracted method for path filtering
352
+ ignored: (filePath: string) => this.shouldIgnorePath(filePath, watchedDir, smythPath),
262
353
  persistent: true,
263
354
  ignoreInitial: true, // Don't fire events for files that already exist
264
355
  awaitWriteFinish: {