@thelogicatelier/sylva 1.0.13 → 1.0.14

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.
@@ -49,6 +49,8 @@ const BRAVE_SEARCH_URL = "https://api.search.brave.com/res/v1/web/search";
49
49
  function cacheKey(query) {
50
50
  return crypto.createHash("md5").update(query).digest("hex");
51
51
  }
52
+ let lastRequestTime = 0;
53
+ const RATE_LIMIT_MS = 1100; // 1.1 seconds to be safe
52
54
  /**
53
55
  * Search Brave API for a query. Returns parsed results.
54
56
  * Caches results to disk if cacheDir is provided.
@@ -74,6 +76,12 @@ async function braveSearch(query, options) {
74
76
  }
75
77
  try {
76
78
  const url = `${BRAVE_SEARCH_URL}?q=${encodeURIComponent(query)}&count=5`;
79
+ const now = Date.now();
80
+ const timeSinceLast = now - lastRequestTime;
81
+ if (timeSinceLast < RATE_LIMIT_MS) {
82
+ await new Promise((resolve) => setTimeout(resolve, RATE_LIMIT_MS - timeSinceLast));
83
+ }
84
+ lastRequestTime = Date.now();
77
85
  const response = await fetch(url, {
78
86
  headers: {
79
87
  Accept: "application/json",
@@ -81,6 +89,7 @@ async function braveSearch(query, options) {
81
89
  "X-Subscription-Token": apiKey,
82
90
  },
83
91
  });
92
+ lastRequestTime = Date.now();
84
93
  if (!response.ok) {
85
94
  const statusText = response.statusText || "Unknown error";
86
95
  const errorMsg = `HTTP ${response.status} (${statusText})`;
@@ -43,6 +43,15 @@ const ignore_1 = __importDefault(require("ignore"));
43
43
  const constants_1 = require("../constants");
44
44
  const MAX_SCAN_FILES = 200;
45
45
  const MAX_LINES_PER_FILE = 500;
46
+ // Precompute known config files and docker files for fast lookup
47
+ const CONFIG_FILE_NAMES = new Set();
48
+ for (const int of constants_1.INTEGRATIONS) {
49
+ if (int.configPatterns) {
50
+ for (const pat of int.configPatterns)
51
+ CONFIG_FILE_NAMES.add(pat);
52
+ }
53
+ }
54
+ const DOCKER_FILE_NAMES = new Set(["docker-compose.yml", "docker-compose.yaml"]);
46
55
  /**
47
56
  * Loads .gitignore rules from the root if available
48
57
  */
@@ -92,7 +101,12 @@ function collectSourceFiles(dir, ig, rootPath, files = []) {
92
101
  }
93
102
  else if (entry.isFile()) {
94
103
  const ext = path.extname(entry.name).toLowerCase();
95
- if (constants_1.SOURCE_EXTENSIONS.includes(ext)) {
104
+ const baseName = entry.name;
105
+ const lowerBaseName = baseName.toLowerCase();
106
+ if (constants_1.SOURCE_EXTENSIONS.includes(ext) ||
107
+ CONFIG_FILE_NAMES.has(baseName) ||
108
+ DOCKER_FILE_NAMES.has(lowerBaseName) ||
109
+ lowerBaseName.includes("dockerfile")) {
96
110
  files.push(fullPath);
97
111
  }
98
112
  }
@@ -123,6 +137,31 @@ function scanSourceFiles(rootPath) {
123
137
  const detectedIntegrationIds = new Set();
124
138
  for (const file of targetFiles) {
125
139
  try {
140
+ const fileName = path.basename(file);
141
+ const lowerFileName = fileName.toLowerCase();
142
+ const isDocker = lowerFileName.includes("dockerfile") || DOCKER_FILE_NAMES.has(lowerFileName);
143
+ // 1. Check exact config file matches (no file reading required)
144
+ for (const integration of constants_1.INTEGRATIONS) {
145
+ if (detectedIntegrationIds.has(integration.id))
146
+ continue;
147
+ if (integration.configPatterns?.includes(fileName)) {
148
+ detectedIntegrationIds.add(integration.id);
149
+ signals.push({
150
+ kind: "integration",
151
+ frameworkId: integration.id,
152
+ frameworkName: integration.name,
153
+ version: { certainty: "unknown", value: undefined },
154
+ evidence: {
155
+ file: path.relative(rootPath, file),
156
+ reason: `Source code scanner detected ${integration.name}: Found config file "${fileName}"`,
157
+ },
158
+ scope: { pathRoot: rootPath },
159
+ });
160
+ }
161
+ }
162
+ // If we found everything, stop scanning
163
+ if (detectedIntegrationIds.size === constants_1.INTEGRATIONS.length)
164
+ break;
126
165
  // Read file and limit to N lines to keep it fast
127
166
  const contentRaw = fs.readFileSync(file, "utf-8");
128
167
  let content = contentRaw;
@@ -130,25 +169,35 @@ function scanSourceFiles(rootPath) {
130
169
  if (lines.length > MAX_LINES_PER_FILE) {
131
170
  content = lines.slice(0, MAX_LINES_PER_FILE).join("\n");
132
171
  }
133
- // Check all integrations against this chunk
172
+ // 2. Content Checks
134
173
  for (const integration of constants_1.INTEGRATIONS) {
135
174
  if (detectedIntegrationIds.has(integration.id))
136
175
  continue; // Found it already somewhere
137
176
  let reason = "";
138
177
  let matchedPattern = "";
139
- matchedPattern = matchPatterns(content, integration.urlPatterns) || "";
140
- if (matchedPattern) {
141
- reason = `Found API URL pattern "${matchedPattern}"`;
178
+ if (isDocker) {
179
+ // Specialized Dockerfile scanning: scan the exact full content
180
+ matchedPattern = matchPatterns(contentRaw, integration.dockerPatterns) || "";
181
+ if (matchedPattern) {
182
+ reason = `Found deployment marker "${matchedPattern}" in ${fileName}`;
183
+ }
142
184
  }
143
185
  else {
144
- matchedPattern = matchPatterns(content, integration.importPatterns) || "";
186
+ // Standard source code checks
187
+ matchedPattern = matchPatterns(content, integration.urlPatterns) || "";
145
188
  if (matchedPattern) {
146
- reason = `Found SDK import pattern "${matchedPattern}"`;
189
+ reason = `Found API URL pattern "${matchedPattern}"`;
147
190
  }
148
191
  else {
149
- matchedPattern = matchPatterns(content, integration.envPatterns) || "";
192
+ matchedPattern = matchPatterns(content, integration.importPatterns) || "";
150
193
  if (matchedPattern) {
151
- reason = `Found env var reference "${matchedPattern}"`;
194
+ reason = `Found SDK import pattern "${matchedPattern}"`;
195
+ }
196
+ else {
197
+ matchedPattern = matchPatterns(content, integration.envPatterns) || "";
198
+ if (matchedPattern) {
199
+ reason = `Found env var reference "${matchedPattern}"`;
200
+ }
152
201
  }
153
202
  }
154
203
  }
@@ -25,6 +25,8 @@ export interface IntegrationDef {
25
25
  urlPatterns?: string[];
26
26
  importPatterns?: string[];
27
27
  envPatterns?: string[];
28
+ configPatterns?: string[];
29
+ dockerPatterns?: string[];
28
30
  }
29
31
  export declare const INTEGRATIONS: IntegrationDef[];
30
32
  export declare const SOURCE_EXTENSIONS: string[];
package/dist/constants.js CHANGED
@@ -154,41 +154,73 @@ exports.INTEGRATIONS = [
154
154
  id: "aws",
155
155
  name: "AWS",
156
156
  importPatterns: ["boto3", "aws-sdk", "@aws-sdk"],
157
+ configPatterns: ["buildspec.yml", "samconfig.toml", "serverless.yml"],
157
158
  envPatterns: ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_REGION", "AWS_DEFAULT_REGION"],
159
+ dockerPatterns: ["public.ecr.aws", "amazonaws.com"],
158
160
  },
159
161
  {
160
162
  id: "azure",
161
163
  name: "Microsoft Azure",
162
164
  importPatterns: ["azure", "@azure"],
163
165
  urlPatterns: ["windows.net", "azure.com"],
166
+ configPatterns: ["azure-pipelines.yml"],
164
167
  envPatterns: ["AZURE_CLIENT_ID", "AZURE_TENANT_ID"],
168
+ dockerPatterns: ["mcr.microsoft.com", "azurecr.io"],
165
169
  },
166
170
  {
167
171
  id: "gcp",
168
172
  name: "Google Cloud",
169
173
  importPatterns: ["google-cloud", "@google-cloud"],
170
174
  urlPatterns: ["googleapis.com"],
175
+ configPatterns: ["app.yaml", "cloudbuild.yaml"],
171
176
  envPatterns: ["GOOGLE_APPLICATION_CREDENTIALS"],
177
+ dockerPatterns: ["gcr.io", "pkg.dev"],
172
178
  },
173
179
  // Hosting & Edge
174
180
  {
175
181
  id: "vercel",
176
182
  name: "Vercel",
177
183
  importPatterns: ["@vercel"],
184
+ configPatterns: ["vercel.json"],
178
185
  envPatterns: ["VERCEL_URL", "VERCEL_PROJECT_ID"],
179
186
  },
180
187
  {
181
188
  id: "netlify",
182
189
  name: "Netlify",
183
190
  importPatterns: ["@netlify"],
184
- envPatterns: ["NETLIFY", "URL"],
191
+ configPatterns: ["netlify.toml"],
192
+ envPatterns: ["NETLIFY", "NETLIFY_AUTH_TOKEN"], // Removed generic URL
185
193
  },
186
194
  {
187
195
  id: "cloudflare",
188
196
  name: "Cloudflare",
189
197
  importPatterns: ["cloudflare", "@cloudflare"],
198
+ configPatterns: ["wrangler.toml"],
190
199
  envPatterns: ["CLOUDFLARE_API_TOKEN", "CLOUDFLARE_ACCOUNT_ID"],
191
200
  },
201
+ {
202
+ id: "flyio",
203
+ name: "Fly.io",
204
+ configPatterns: ["fly.toml"],
205
+ dockerPatterns: ["flyio", "flyctl"],
206
+ },
207
+ {
208
+ id: "railway",
209
+ name: "Railway",
210
+ configPatterns: ["railway.json", "railway.toml"],
211
+ dockerPatterns: ["railwayapp"],
212
+ },
213
+ {
214
+ id: "render",
215
+ name: "Render",
216
+ configPatterns: ["render.yaml"],
217
+ },
218
+ {
219
+ id: "digitalocean",
220
+ name: "DigitalOcean",
221
+ configPatterns: ["app-spec.yaml", "digitalocean.yaml"],
222
+ dockerPatterns: ["digitalocean"],
223
+ },
192
224
  // Firebase
193
225
  {
194
226
  id: "firebase",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thelogicatelier/sylva",
3
- "version": "1.0.13",
3
+ "version": "1.0.14",
4
4
  "description": "Auto-generate AGENTS.md for your repository using Ax-LLM. Analyze the structural backbone, data flow, and day-to-day coding conventions natively.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -64,4 +64,4 @@
64
64
  "prettier": "^3.8.1",
65
65
  "typescript-eslint": "^8.56.1"
66
66
  }
67
- }
67
+ }