@justmpm/ai-tool 0.5.1 → 0.5.4

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.
package/README.md CHANGED
@@ -155,6 +155,46 @@ Cria `.analyze/areas.config.json` com:
155
155
  - Patterns glob para cada area
156
156
  - Keywords de deteccao
157
157
  - Descricoes manuais de arquivos
158
+ - Padroes de ignore global
159
+
160
+ ### Configuracao de Areas
161
+
162
+ O arquivo `.analyze/areas.config.json` suporta:
163
+
164
+ ```json
165
+ {
166
+ "$schema": "./areas.schema.json",
167
+ "version": "1.0.0",
168
+ "ignore": [
169
+ "docs/brainstorming/**",
170
+ "functions/lib/**",
171
+ "**/*.test.ts"
172
+ ],
173
+ "areas": {
174
+ "auth": {
175
+ "name": "Autenticacao",
176
+ "description": "Sistema de login e sessao",
177
+ "patterns": ["**/auth/**", "**/login/**"],
178
+ "keywords": ["auth", "login", "session"]
179
+ }
180
+ },
181
+ "descriptions": {
182
+ "src/hooks/useAuth.ts": "Hook principal de autenticacao"
183
+ },
184
+ "settings": {
185
+ "autoDetect": true,
186
+ "inferDescriptions": true
187
+ }
188
+ }
189
+ ```
190
+
191
+ | Campo | Descricao |
192
+ |-------|-----------|
193
+ | `ignore` | Padroes glob para ignorar arquivos/pastas globalmente |
194
+ | `areas` | Definicao manual de areas com patterns e keywords |
195
+ | `descriptions` | Descricoes manuais para arquivos especificos |
196
+ | `settings.autoDetect` | Se `false`, usa apenas areas definidas manualmente |
197
+ | `settings.inferDescriptions` | Infere descricoes automaticamente baseado no nome |
158
198
 
159
199
  ## Servidor MCP
160
200
 
@@ -1079,6 +1079,7 @@ var CONFIG_FILE = "areas.config.json";
1079
1079
  var DEFAULT_CONFIG = {
1080
1080
  $schema: "./areas.schema.json",
1081
1081
  version: "1.0.0",
1082
+ ignore: [],
1082
1083
  areas: {},
1083
1084
  descriptions: {},
1084
1085
  settings: {
@@ -1475,6 +1476,17 @@ var AREA_DESCRIPTIONS = {
1475
1476
  };
1476
1477
 
1477
1478
  // src/areas/detector.ts
1479
+ function isFileIgnored(filePath, config) {
1480
+ const ignorePatterns = config.ignore || [];
1481
+ if (ignorePatterns.length === 0) return false;
1482
+ const normalizedPath = filePath.replace(/\\/g, "/");
1483
+ for (const pattern of ignorePatterns) {
1484
+ if (minimatch(normalizedPath, pattern, { dot: true })) {
1485
+ return true;
1486
+ }
1487
+ }
1488
+ return false;
1489
+ }
1478
1490
  function detectFileAreas(filePath, config) {
1479
1491
  const normalizedPath = filePath.replace(/\\/g, "/");
1480
1492
  const matches = [];
@@ -1722,6 +1734,9 @@ function detectAreasInfo(cwd, filePaths) {
1722
1734
  const areaSet = /* @__PURE__ */ new Set();
1723
1735
  let unmappedCount = 0;
1724
1736
  for (const filePath of filePaths) {
1737
+ if (isFileIgnored(filePath, config)) {
1738
+ continue;
1739
+ }
1725
1740
  const areas2 = detectFileAreas(filePath, config);
1726
1741
  if (areas2.length === 0) {
1727
1742
  unmappedCount++;
@@ -1858,6 +1873,254 @@ ${output}`;
1858
1873
 
1859
1874
  // src/commands/impact.ts
1860
1875
  import skott2 from "skott";
1876
+
1877
+ // src/utils/similarity.ts
1878
+ function levenshteinDistance(a, b) {
1879
+ const matrix = [];
1880
+ for (let i = 0; i <= b.length; i++) {
1881
+ matrix[i] = [i];
1882
+ }
1883
+ for (let j = 0; j <= a.length; j++) {
1884
+ matrix[0][j] = j;
1885
+ }
1886
+ for (let i = 1; i <= b.length; i++) {
1887
+ for (let j = 1; j <= a.length; j++) {
1888
+ if (b.charAt(i - 1) === a.charAt(j - 1)) {
1889
+ matrix[i][j] = matrix[i - 1][j - 1];
1890
+ } else {
1891
+ matrix[i][j] = Math.min(
1892
+ matrix[i - 1][j - 1] + 1,
1893
+ // substituição
1894
+ matrix[i][j - 1] + 1,
1895
+ // inserção
1896
+ matrix[i - 1][j] + 1
1897
+ // deleção
1898
+ );
1899
+ }
1900
+ }
1901
+ }
1902
+ return matrix[b.length][a.length];
1903
+ }
1904
+ function findSimilar(target, candidates, options = {}) {
1905
+ const {
1906
+ maxDistance = 3,
1907
+ limit = 5,
1908
+ normalize: normalize2 = true,
1909
+ extractKey = (item) => String(item)
1910
+ } = options;
1911
+ const normalizedTarget = normalize2 ? target.toLowerCase() : target;
1912
+ const scored = candidates.map((item) => {
1913
+ const key = extractKey(item);
1914
+ const normalizedKey = normalize2 ? key.toLowerCase() : key;
1915
+ const keyNoExt = normalizedKey.replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
1916
+ let score = Infinity;
1917
+ if (normalizedKey.includes(normalizedTarget) || keyNoExt.includes(normalizedTarget)) {
1918
+ score = 0;
1919
+ } else {
1920
+ score = levenshteinDistance(keyNoExt, normalizedTarget);
1921
+ }
1922
+ return { item, score };
1923
+ }).filter(({ score }) => score <= maxDistance).sort((a, b) => a.score - b.score).slice(0, limit);
1924
+ return scored.map(({ item }) => item);
1925
+ }
1926
+ function findBestMatch(target, candidates, extractKey = (item) => String(item)) {
1927
+ const similar = findSimilar(target, candidates, {
1928
+ maxDistance: 2,
1929
+ // Mais restritivo para "você quis dizer"
1930
+ limit: 1,
1931
+ extractKey
1932
+ });
1933
+ return similar.length > 0 ? similar[0] : null;
1934
+ }
1935
+ function extractFileName(filePath) {
1936
+ const fileName = filePath.split("/").pop() || filePath;
1937
+ return fileName.replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
1938
+ }
1939
+
1940
+ // src/utils/errors.ts
1941
+ var COMMAND_REFERENCE = {
1942
+ map: "Resumo do projeto (sem target)",
1943
+ areas: "Listar todas as \xE1reas (sem target)",
1944
+ area: "Arquivos de uma \xE1rea espec\xEDfica",
1945
+ suggest: "O que ler antes de editar",
1946
+ context: "API/assinaturas de um arquivo",
1947
+ impact: "Quem usa este arquivo",
1948
+ dead: "C\xF3digo morto (sem target)"
1949
+ };
1950
+ function getCommandReferenceSection(excludeCommand) {
1951
+ let out = `
1952
+ \u{1F4CC} Comandos \xFAteis:
1953
+ `;
1954
+ for (const [cmd, desc] of Object.entries(COMMAND_REFERENCE)) {
1955
+ if (cmd !== excludeCommand) {
1956
+ out += ` ai-tool ${cmd.padEnd(10)} ${desc}
1957
+ `;
1958
+ }
1959
+ }
1960
+ return out;
1961
+ }
1962
+ function formatFileNotFound(options) {
1963
+ const { target, allFiles, command } = options;
1964
+ const similarFiles = findSimilar(target, allFiles, {
1965
+ maxDistance: 3,
1966
+ limit: 5,
1967
+ extractKey: extractFileName
1968
+ });
1969
+ const bestMatch = findBestMatch(target, allFiles, extractFileName);
1970
+ let out = `
1971
+ \u274C Arquivo n\xE3o encontrado: "${target}"
1972
+
1973
+ `;
1974
+ out += `\u{1F4CA} Total de arquivos indexados: ${allFiles.length}
1975
+
1976
+ `;
1977
+ if (bestMatch) {
1978
+ out += `\u{1F4A1} Voc\xEA quis dizer?
1979
+ `;
1980
+ out += ` \u2192 ${bestMatch}
1981
+
1982
+ `;
1983
+ }
1984
+ if (similarFiles.length > 1) {
1985
+ out += `\u{1F4DD} Arquivos similares:
1986
+ `;
1987
+ for (const f of similarFiles) {
1988
+ if (f !== bestMatch) {
1989
+ out += ` \u2022 ${f}
1990
+ `;
1991
+ }
1992
+ }
1993
+ out += "\n";
1994
+ }
1995
+ out += `\u{1F4D6} Dicas:
1996
+ `;
1997
+ out += ` \u2022 Use o caminho relativo: src/components/Header.tsx
1998
+ `;
1999
+ out += ` \u2022 Ou apenas o nome do arquivo: Header
2000
+ `;
2001
+ out += ` \u2022 Verifique se o arquivo est\xE1 em uma pasta inclu\xEDda no scan
2002
+ `;
2003
+ if (command) {
2004
+ out += getCommandReferenceSection(command);
2005
+ }
2006
+ return out;
2007
+ }
2008
+ function formatAreaNotFound(options) {
2009
+ const { target, availableAreas } = options;
2010
+ const areaIds = availableAreas.map((a) => a.id);
2011
+ const bestMatchId = findBestMatch(target, areaIds);
2012
+ const similarAreaIds = findSimilar(target, areaIds, {
2013
+ maxDistance: 3,
2014
+ limit: 5
2015
+ });
2016
+ let out = `
2017
+ \u274C \xC1rea n\xE3o encontrada: "${target}"
2018
+
2019
+ `;
2020
+ if (bestMatchId) {
2021
+ out += `\u{1F4A1} Voc\xEA quis dizer?
2022
+ `;
2023
+ out += ` \u2192 ai-tool area ${bestMatchId}
2024
+
2025
+ `;
2026
+ }
2027
+ if (availableAreas.length > 0) {
2028
+ out += `\u{1F4E6} \xC1reas dispon\xEDveis:
2029
+
2030
+ `;
2031
+ if (similarAreaIds.length > 0 && !bestMatchId) {
2032
+ for (const id of similarAreaIds) {
2033
+ const area2 = availableAreas.find((a) => a.id === id);
2034
+ if (area2) {
2035
+ out += ` ${area2.id.padEnd(25)} ${area2.count} arquivos
2036
+ `;
2037
+ }
2038
+ }
2039
+ out += ` ---
2040
+ `;
2041
+ }
2042
+ const areasToShow = similarAreaIds.length > 0 && !bestMatchId ? availableAreas.filter((a) => !similarAreaIds.includes(a.id)).slice(0, 10) : availableAreas.slice(0, 15);
2043
+ for (const { id, count } of areasToShow) {
2044
+ out += ` ${id.padEnd(25)} ${count} arquivos
2045
+ `;
2046
+ }
2047
+ const totalShown = similarAreaIds.length > 0 && !bestMatchId ? similarAreaIds.length + areasToShow.length : areasToShow.length;
2048
+ if (availableAreas.length > totalShown) {
2049
+ out += ` ... e mais ${availableAreas.length - totalShown}
2050
+ `;
2051
+ }
2052
+ out += `
2053
+ `;
2054
+ }
2055
+ out += `\u{1F4D6} Dicas:
2056
+ `;
2057
+ out += ` \u2022 Use o ID exato da \xE1rea (ex: ai-tool area auth)
2058
+ `;
2059
+ out += ` \u2022 Use 'ai-tool areas' para listar todas as \xE1reas
2060
+ `;
2061
+ out += ` \u2022 IDs s\xE3o case-sensitive (Auth \u2260 auth)
2062
+ `;
2063
+ out += `
2064
+ \u{1F4CC} Comandos relacionados:
2065
+ `;
2066
+ out += ` ai-tool areas Listar todas as \xE1reas
2067
+ `;
2068
+ out += ` ai-tool map Ver estrutura do projeto
2069
+ `;
2070
+ return out;
2071
+ }
2072
+ function formatMissingTarget(command) {
2073
+ let out = `
2074
+ \u274C Erro: par\xE2metro "target" \xE9 OBRIGAT\xD3RIO para o comando "${command}".
2075
+
2076
+ `;
2077
+ out += `\u{1F4DD} Exemplos:
2078
+ `;
2079
+ if (command === "area") {
2080
+ out += ` ai-tool area auth
2081
+ `;
2082
+ out += ` ai-tool area dashboard
2083
+ `;
2084
+ out += ` ai-tool area billing --type=hook
2085
+
2086
+ `;
2087
+ out += `\u{1F4A1} Use 'ai-tool areas' para listar todas as \xE1reas dispon\xEDveis.
2088
+ `;
2089
+ } else {
2090
+ out += ` ai-tool ${command} useAuth
2091
+ `;
2092
+ out += ` ai-tool ${command} Button.tsx
2093
+ `;
2094
+ out += ` ai-tool ${command} src/hooks/useAuth.ts
2095
+ `;
2096
+ }
2097
+ out += getCommandReferenceSection(command);
2098
+ return out;
2099
+ }
2100
+ function formatInvalidCommand(command) {
2101
+ const validCommands = Object.keys(COMMAND_REFERENCE);
2102
+ const bestMatch = findBestMatch(command, validCommands);
2103
+ let out = `
2104
+ \u274C Comando inv\xE1lido: "${command}"
2105
+
2106
+ `;
2107
+ if (bestMatch) {
2108
+ out += `\u{1F4A1} Voc\xEA quis dizer?
2109
+ `;
2110
+ out += ` \u2192 ai-tool ${bestMatch}
2111
+
2112
+ `;
2113
+ }
2114
+ out += `\u{1F4CC} Comandos dispon\xEDveis:
2115
+ `;
2116
+ for (const [cmd, desc] of Object.entries(COMMAND_REFERENCE)) {
2117
+ out += ` ai-tool ${cmd.padEnd(10)} ${desc}
2118
+ `;
2119
+ }
2120
+ return out;
2121
+ }
2122
+
2123
+ // src/commands/impact.ts
1861
2124
  async function impact(target, options = {}) {
1862
2125
  const cwd = options.cwd || process.cwd();
1863
2126
  const format = options.format || "text";
@@ -2050,37 +2313,7 @@ function findTargetFile(target, allFiles) {
2050
2313
  return null;
2051
2314
  }
2052
2315
  function formatNotFound(target, allFiles) {
2053
- const normalizedTarget = target.toLowerCase();
2054
- const similar = allFiles.filter((f) => {
2055
- const fileName = f.split("/").pop()?.toLowerCase() || "";
2056
- const fileNameNoExt = fileName.replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
2057
- return fileName.includes(normalizedTarget) || fileNameNoExt.includes(normalizedTarget) || levenshteinDistance(fileNameNoExt, normalizedTarget) <= 3;
2058
- }).slice(0, 5);
2059
- let out = `\u274C Arquivo n\xE3o encontrado no \xEDndice: "${target}"
2060
-
2061
- `;
2062
- out += `\u{1F4CA} Total de arquivos indexados: ${allFiles.length}
2063
-
2064
- `;
2065
- if (similar.length > 0) {
2066
- out += `\u{1F4DD} Arquivos com nome similar:
2067
- `;
2068
- for (const s of similar) {
2069
- out += ` \u2022 ${s}
2070
- `;
2071
- }
2072
- out += `
2073
- `;
2074
- }
2075
- out += `\u{1F4A1} Dicas:
2076
- `;
2077
- out += ` \u2022 Use o caminho relativo: src/components/Header.tsx
2078
- `;
2079
- out += ` \u2022 Ou apenas o nome do arquivo: Header
2080
- `;
2081
- out += ` \u2022 Verifique se o arquivo est\xE1 em uma pasta inclu\xEDda no scan
2082
- `;
2083
- return out;
2316
+ return formatFileNotFound({ target, allFiles, command: "impact" });
2084
2317
  }
2085
2318
  function toImpactFile(isDirect) {
2086
2319
  return (path) => ({
@@ -2149,29 +2382,6 @@ function generateSuggestions(upstream, downstream, risks) {
2149
2382
  }
2150
2383
  return suggestions;
2151
2384
  }
2152
- function levenshteinDistance(a, b) {
2153
- const matrix = [];
2154
- for (let i = 0; i <= b.length; i++) {
2155
- matrix[i] = [i];
2156
- }
2157
- for (let j = 0; j <= a.length; j++) {
2158
- matrix[0][j] = j;
2159
- }
2160
- for (let i = 1; i <= b.length; i++) {
2161
- for (let j = 1; j <= a.length; j++) {
2162
- if (b.charAt(i - 1) === a.charAt(j - 1)) {
2163
- matrix[i][j] = matrix[i - 1][j - 1];
2164
- } else {
2165
- matrix[i][j] = Math.min(
2166
- matrix[i - 1][j - 1] + 1,
2167
- matrix[i][j - 1] + 1,
2168
- matrix[i - 1][j] + 1
2169
- );
2170
- }
2171
- }
2172
- }
2173
- return matrix[b.length][a.length];
2174
- }
2175
2385
 
2176
2386
  // src/commands/suggest.ts
2177
2387
  import skott3 from "skott";
@@ -2350,60 +2560,7 @@ function findTargetFile2(target, allFiles) {
2350
2560
  return null;
2351
2561
  }
2352
2562
  function formatNotFound2(target, allFiles) {
2353
- const normalizedTarget = target.toLowerCase();
2354
- const similar = allFiles.filter((f) => {
2355
- const fileName = f.split("/").pop()?.toLowerCase() || "";
2356
- const fileNameNoExt = fileName.replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
2357
- return fileName.includes(normalizedTarget) || fileNameNoExt.includes(normalizedTarget) || levenshteinDistance2(fileNameNoExt, normalizedTarget) <= 3;
2358
- }).slice(0, 5);
2359
- let out = `Arquivo nao encontrado no indice: "${target}"
2360
-
2361
- `;
2362
- out += `Total de arquivos indexados: ${allFiles.length}
2363
-
2364
- `;
2365
- if (similar.length > 0) {
2366
- out += `Arquivos com nome similar:
2367
- `;
2368
- for (const s of similar) {
2369
- out += ` - ${s}
2370
- `;
2371
- }
2372
- out += `
2373
- `;
2374
- }
2375
- out += `Dicas:
2376
- `;
2377
- out += ` - Use o caminho relativo: src/components/Header.tsx
2378
- `;
2379
- out += ` - Ou apenas o nome do arquivo: Header
2380
- `;
2381
- out += ` - Verifique se o arquivo esta em uma pasta incluida no scan
2382
- `;
2383
- return out;
2384
- }
2385
- function levenshteinDistance2(a, b) {
2386
- const matrix = [];
2387
- for (let i = 0; i <= b.length; i++) {
2388
- matrix[i] = [i];
2389
- }
2390
- for (let j = 0; j <= a.length; j++) {
2391
- matrix[0][j] = j;
2392
- }
2393
- for (let i = 1; i <= b.length; i++) {
2394
- for (let j = 1; j <= a.length; j++) {
2395
- if (b.charAt(i - 1) === a.charAt(j - 1)) {
2396
- matrix[i][j] = matrix[i - 1][j - 1];
2397
- } else {
2398
- matrix[i][j] = Math.min(
2399
- matrix[i - 1][j - 1] + 1,
2400
- matrix[i][j - 1] + 1,
2401
- matrix[i - 1][j] + 1
2402
- );
2403
- }
2404
- }
2405
- }
2406
- return matrix[b.length][a.length];
2563
+ return formatFileNotFound({ target, allFiles, command: "suggest" });
2407
2564
  }
2408
2565
 
2409
2566
  // src/commands/context.ts
@@ -2725,61 +2882,8 @@ function shouldIgnore(name) {
2725
2882
  return ignoredDirs.includes(name) || name.startsWith(".");
2726
2883
  }
2727
2884
  function formatNotFound3(target, cwd) {
2728
- const normalizedTarget = target.toLowerCase();
2729
2885
  const allFiles = getAllCodeFiles(cwd);
2730
- const similar = allFiles.filter((f) => {
2731
- const fileName = basename(f).toLowerCase();
2732
- const fileNameNoExt = fileName.replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
2733
- return fileName.includes(normalizedTarget) || fileNameNoExt.includes(normalizedTarget) || levenshteinDistance3(fileNameNoExt, normalizedTarget) <= 3;
2734
- }).slice(0, 5);
2735
- let out = `Arquivo nao encontrado: "${target}"
2736
-
2737
- `;
2738
- out += `Total de arquivos no projeto: ${allFiles.length}
2739
-
2740
- `;
2741
- if (similar.length > 0) {
2742
- out += `Arquivos com nome similar:
2743
- `;
2744
- for (const s of similar) {
2745
- out += ` - ${s}
2746
- `;
2747
- }
2748
- out += `
2749
- `;
2750
- }
2751
- out += `Dicas:
2752
- `;
2753
- out += ` - Use o caminho relativo: src/components/Header.tsx
2754
- `;
2755
- out += ` - Ou apenas o nome do arquivo: Header
2756
- `;
2757
- out += ` - Verifique se o arquivo existe e e um arquivo .ts/.tsx/.js/.jsx
2758
- `;
2759
- return out;
2760
- }
2761
- function levenshteinDistance3(a, b) {
2762
- const matrix = [];
2763
- for (let i = 0; i <= b.length; i++) {
2764
- matrix[i] = [i];
2765
- }
2766
- for (let j = 0; j <= a.length; j++) {
2767
- matrix[0][j] = j;
2768
- }
2769
- for (let i = 1; i <= b.length; i++) {
2770
- for (let j = 1; j <= a.length; j++) {
2771
- if (b.charAt(i - 1) === a.charAt(j - 1)) {
2772
- matrix[i][j] = matrix[i - 1][j - 1];
2773
- } else {
2774
- matrix[i][j] = Math.min(
2775
- matrix[i - 1][j - 1] + 1,
2776
- matrix[i][j - 1] + 1,
2777
- matrix[i - 1][j] + 1
2778
- );
2779
- }
2780
- }
2781
- }
2782
- return matrix[b.length][a.length];
2886
+ return formatFileNotFound({ target, allFiles, command: "context" });
2783
2887
  }
2784
2888
 
2785
2889
  // src/commands/areas.ts
@@ -2804,9 +2908,10 @@ async function areas(options = {}) {
2804
2908
  try {
2805
2909
  const config = readConfig(cwd);
2806
2910
  const allFiles = getAllCodeFiles2(cwd);
2911
+ const filteredFiles = allFiles.filter((filePath) => !isFileIgnored(filePath, config));
2807
2912
  const areaMap = /* @__PURE__ */ new Map();
2808
2913
  const unmapped = [];
2809
- for (const filePath of allFiles) {
2914
+ for (const filePath of filteredFiles) {
2810
2915
  const category = detectCategory(filePath);
2811
2916
  const areas2 = detectFileAreas(filePath, config);
2812
2917
  let description = getFileDescription(cwd, filePath);
@@ -2854,7 +2959,7 @@ async function areas(options = {}) {
2854
2959
  unmapped,
2855
2960
  summary: {
2856
2961
  totalAreas: detectedAreas.length,
2857
- totalFiles: allFiles.length,
2962
+ totalFiles: filteredFiles.length,
2858
2963
  unmappedCount: unmapped.length
2859
2964
  }
2860
2965
  };
@@ -2921,9 +3026,10 @@ async function area(target, options = {}) {
2921
3026
  try {
2922
3027
  const config = readConfig(cwd);
2923
3028
  const allFiles = getAllCodeFiles3(cwd);
3029
+ const filteredFiles = allFiles.filter((filePath) => !isFileIgnored(filePath, config));
2924
3030
  const areaFiles = [];
2925
3031
  const targetLower = target.toLowerCase();
2926
- for (const filePath of allFiles) {
3032
+ for (const filePath of filteredFiles) {
2927
3033
  const fileAreas = detectFileAreas(filePath, config);
2928
3034
  const belongsToArea = fileAreas.some(
2929
3035
  (a) => a.toLowerCase() === targetLower || a.toLowerCase().includes(targetLower)
@@ -2945,8 +3051,8 @@ async function area(target, options = {}) {
2945
3051
  }
2946
3052
  }
2947
3053
  if (areaFiles.length === 0) {
2948
- const availableAreas = getAvailableAreas(allFiles, config);
2949
- return formatAreaNotFound(target, availableAreas);
3054
+ const availableAreas = getAvailableAreas(filteredFiles, config);
3055
+ return formatAreaNotFound2(target, availableAreas);
2950
3056
  }
2951
3057
  const byCategory = {};
2952
3058
  const categories = {};
@@ -2960,7 +3066,7 @@ async function area(target, options = {}) {
2960
3066
  for (const cat of Object.keys(byCategory)) {
2961
3067
  byCategory[cat].sort((a, b) => a.path.localeCompare(b.path));
2962
3068
  }
2963
- const realAreaId = findRealAreaId(target, allFiles, config);
3069
+ const realAreaId = findRealAreaId(target, filteredFiles, config);
2964
3070
  const detectedArea = {
2965
3071
  id: realAreaId || target,
2966
3072
  name: getAreaName(realAreaId || target, config),
@@ -3013,33 +3119,8 @@ function getAvailableAreas(allFiles, config) {
3013
3119
  }
3014
3120
  return [...areaCounts.entries()].map(([id, count]) => ({ id, count })).sort((a, b) => b.count - a.count);
3015
3121
  }
3016
- function formatAreaNotFound(target, availableAreas) {
3017
- let out = `
3018
- \u274C \xC1rea n\xE3o encontrada: "${target}"
3019
-
3020
- `;
3021
- if (availableAreas.length > 0) {
3022
- out += `\u{1F4E6} \xC1reas dispon\xEDveis:
3023
-
3024
- `;
3025
- for (const { id, count } of availableAreas.slice(0, 15)) {
3026
- out += ` ${id.padEnd(25)} ${count} arquivos
3027
- `;
3028
- }
3029
- if (availableAreas.length > 15) {
3030
- out += ` ... e mais ${availableAreas.length - 15}
3031
- `;
3032
- }
3033
- out += `
3034
- `;
3035
- }
3036
- out += `\u{1F4A1} Dicas:
3037
- `;
3038
- out += ` - Use o ID exato da \xE1rea (ex: ai-tool area auth)
3039
- `;
3040
- out += ` - Use 'ai-tool areas' para listar todas as \xE1reas
3041
- `;
3042
- return out;
3122
+ function formatAreaNotFound2(target, availableAreas) {
3123
+ return formatAreaNotFound({ target, availableAreas });
3043
3124
  }
3044
3125
  function getAllCodeFiles3(dir, files = [], baseDir = dir) {
3045
3126
  try {
@@ -3261,6 +3342,15 @@ export {
3261
3342
  map,
3262
3343
  dead,
3263
3344
  deadFix,
3345
+ levenshteinDistance,
3346
+ findSimilar,
3347
+ findBestMatch,
3348
+ extractFileName,
3349
+ COMMAND_REFERENCE,
3350
+ formatFileNotFound,
3351
+ formatAreaNotFound,
3352
+ formatMissingTarget,
3353
+ formatInvalidCommand,
3264
3354
  impact,
3265
3355
  suggest,
3266
3356
  context,
package/dist/cli.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  impact,
11
11
  map,
12
12
  suggest
13
- } from "./chunk-A3QVUKVA.js";
13
+ } from "./chunk-PHTWZ2JT.js";
14
14
 
15
15
  // src/cli.ts
16
16
  var HELP = `
@@ -87,7 +87,7 @@ async function main() {
87
87
  }
88
88
  }
89
89
  if (flags.mcp) {
90
- const { startMcpServer } = await import("./server-DMLKPY45.js");
90
+ const { startMcpServer } = await import("./server-NIEAOPLJ.js");
91
91
  await startMcpServer();
92
92
  return;
93
93
  }
package/dist/index.d.ts CHANGED
@@ -169,6 +169,7 @@ interface AreaConfig {
169
169
  interface AreasConfigFile {
170
170
  $schema?: string;
171
171
  version: string;
172
+ ignore?: string[];
172
173
  areas: Record<string, AreaConfig>;
173
174
  descriptions?: Record<string, string>;
174
175
  settings?: {
@@ -330,6 +331,139 @@ declare function isEntryPoint(filePath: string): boolean;
330
331
  */
331
332
  declare function isCodeFile(filePath: string): boolean;
332
333
 
334
+ /**
335
+ * Funções de similaridade de strings
336
+ *
337
+ * Usa algoritmo de Levenshtein para calcular distância entre strings
338
+ * e encontrar candidatos similares para sugestões "você quis dizer?"
339
+ */
340
+ /**
341
+ * Calcula a distância de Levenshtein entre duas strings
342
+ *
343
+ * A distância representa o número mínimo de operações (inserção, deleção, substituição)
344
+ * necessárias para transformar uma string na outra.
345
+ *
346
+ * @param a - Primeira string
347
+ * @param b - Segunda string
348
+ * @returns Número de edições necessárias
349
+ *
350
+ * @example
351
+ * levenshteinDistance('auth', 'auht') // 2 (troca de posição = 2 substituições)
352
+ * levenshteinDistance('auth', 'auth') // 0 (iguais)
353
+ * levenshteinDistance('auth', 'auths') // 1 (1 inserção)
354
+ */
355
+ declare function levenshteinDistance(a: string, b: string): number;
356
+ /**
357
+ * Opções para busca de itens similares
358
+ */
359
+ interface FindSimilarOptions<T> {
360
+ /** Distância máxima de Levenshtein para considerar similar (default: 3) */
361
+ maxDistance?: number;
362
+ /** Número máximo de resultados (default: 5) */
363
+ limit?: number;
364
+ /** Normalizar para lowercase antes de comparar (default: true) */
365
+ normalize?: boolean;
366
+ /** Função para extrair a chave de comparação de cada item */
367
+ extractKey?: (item: T) => string;
368
+ }
369
+ /**
370
+ * Encontra itens similares a um target
371
+ *
372
+ * Usa Levenshtein distance e match de substring para encontrar candidatos.
373
+ * Substring match tem prioridade (score 0), seguido por Levenshtein.
374
+ *
375
+ * @param target - Termo buscado
376
+ * @param candidates - Lista de candidatos
377
+ * @param options - Configurações opcionais
378
+ * @returns Lista de candidatos similares, ordenados por relevância
379
+ *
380
+ * @example
381
+ * const areas = ['auth', 'dashboard', 'stripe'];
382
+ * findSimilar('auht', areas) // ['auth']
383
+ * findSimilar('dash', areas) // ['dashboard']
384
+ */
385
+ declare function findSimilar<T>(target: string, candidates: T[], options?: FindSimilarOptions<T>): T[];
386
+ /**
387
+ * Encontra o melhor match para sugestão "você quis dizer?"
388
+ *
389
+ * Mais restritivo que findSimilar (maxDistance: 2) para evitar
390
+ * sugestões incorretas.
391
+ *
392
+ * @param target - Termo buscado
393
+ * @param candidates - Lista de candidatos
394
+ * @param extractKey - Função para extrair chave de comparação
395
+ * @returns Melhor match ou null se não houver sugestão confiável
396
+ *
397
+ * @example
398
+ * findBestMatch('auht', ['auth', 'dashboard']) // 'auth'
399
+ * findBestMatch('xyz', ['auth', 'dashboard']) // null
400
+ */
401
+ declare function findBestMatch<T>(target: string, candidates: T[], extractKey?: (item: T) => string): T | null;
402
+ /**
403
+ * Extrai nome do arquivo de um caminho (sem extensão)
404
+ *
405
+ * Útil para comparar arquivos ignorando caminho e extensão.
406
+ *
407
+ * @param filePath - Caminho do arquivo
408
+ * @returns Nome do arquivo sem extensão
409
+ *
410
+ * @example
411
+ * extractFileName('src/hooks/useAuth.ts') // 'useAuth'
412
+ * extractFileName('Button.tsx') // 'Button'
413
+ */
414
+ declare function extractFileName(filePath: string): string;
415
+
416
+ /**
417
+ * Formatadores de mensagens de erro com sugestões inteligentes
418
+ *
419
+ * Todas as funções retornam strings formatadas para exibição,
420
+ * com sugestões "você quis dizer?" e referência de comandos.
421
+ */
422
+ /**
423
+ * Referência rápida de comandos disponíveis
424
+ */
425
+ declare const COMMAND_REFERENCE: Record<string, string>;
426
+ interface FormatFileNotFoundOptions {
427
+ /** Termo buscado */
428
+ target: string;
429
+ /** Lista de todos os arquivos disponíveis */
430
+ allFiles: string[];
431
+ /** Comando que gerou o erro (para contexto) */
432
+ command?: string;
433
+ }
434
+ /**
435
+ * Formata mensagem de arquivo não encontrado
436
+ *
437
+ * Inclui sugestões de arquivos similares e referência de comandos.
438
+ */
439
+ declare function formatFileNotFound(options: FormatFileNotFoundOptions): string;
440
+ interface AreaInfo {
441
+ /** ID da área */
442
+ id: string;
443
+ /** Número de arquivos na área */
444
+ count: number;
445
+ }
446
+ interface FormatAreaNotFoundOptions {
447
+ /** Termo buscado */
448
+ target: string;
449
+ /** Lista de áreas disponíveis */
450
+ availableAreas: AreaInfo[];
451
+ }
452
+ /**
453
+ * Formata mensagem de área não encontrada
454
+ *
455
+ * Inclui sugestões de áreas similares usando Levenshtein.
456
+ */
457
+ declare function formatAreaNotFound(options: FormatAreaNotFoundOptions): string;
458
+ /**
459
+ * Formata mensagem de target obrigatório faltando
460
+ */
461
+ declare function formatMissingTarget(command: string): string;
462
+ /**
463
+ * Formata mensagem de comando inválido
464
+ */
465
+ declare function formatInvalidCommand(command: string): string;
466
+
333
467
  /**
334
468
  * Utilitários para detecção de Firebase Cloud Functions
335
469
  *
@@ -516,4 +650,4 @@ declare const AREA_DESCRIPTIONS: Record<string, string>;
516
650
 
517
651
  declare const VERSION: string;
518
652
 
519
- export { AREA_DESCRIPTIONS, AREA_NAMES, type AreaConfig, type AreaDetailResult, type AreaFile, type AreaOptions, type AreasConfigFile, type AreasOptions, type AreasResult, type CommandOptions, type ContextOptions, type ContextResult, type DeadFile, type DeadOptions, type DeadResult, type DetectedArea, FOLDER_PATTERNS, type FileCategory, type FileInfo, type FolderStats, type FunctionInfo, type ImpactFile, type ImpactOptions, type ImpactResult, type ImportInfo, KEYWORD_PATTERNS, type MapOptions, type MapResult, type OutputFormat, type ParamInfo, type RiskInfo, type SuggestOptions, type SuggestResult, type Suggestion, type SuggestionPriority, type TypeInfo, type TypeKind, VERSION, area, areas, areasInit, categoryIcons, clearFirebaseCache, configExists, context, dead, deadFix, detectCategory, detectFileAreas, filterCloudFunctionsFalsePositives, getAreaDescription, getAreaName, getCacheDir, getFileDescription, hasFirebaseFunctions, impact, inferFileDescription, invalidateCache, isCacheValid, isCodeFile, isEntryPoint, isExportedCloudFunction, isFirebaseProject, map, readConfig, removeArea, setArea, setFileDescription, suggest, writeConfig };
653
+ export { AREA_DESCRIPTIONS, AREA_NAMES, type AreaConfig, type AreaDetailResult, type AreaFile, type AreaInfo, type AreaOptions, type AreasConfigFile, type AreasOptions, type AreasResult, COMMAND_REFERENCE, type CommandOptions, type ContextOptions, type ContextResult, type DeadFile, type DeadOptions, type DeadResult, type DetectedArea, FOLDER_PATTERNS, type FileCategory, type FileInfo, type FindSimilarOptions, type FolderStats, type FormatAreaNotFoundOptions, type FormatFileNotFoundOptions, type FunctionInfo, type ImpactFile, type ImpactOptions, type ImpactResult, type ImportInfo, KEYWORD_PATTERNS, type MapOptions, type MapResult, type OutputFormat, type ParamInfo, type RiskInfo, type SuggestOptions, type SuggestResult, type Suggestion, type SuggestionPriority, type TypeInfo, type TypeKind, VERSION, area, areas, areasInit, categoryIcons, clearFirebaseCache, configExists, context, dead, deadFix, detectCategory, detectFileAreas, extractFileName, filterCloudFunctionsFalsePositives, findBestMatch, findSimilar, formatAreaNotFound, formatFileNotFound, formatInvalidCommand, formatMissingTarget, getAreaDescription, getAreaName, getCacheDir, getFileDescription, hasFirebaseFunctions, impact, inferFileDescription, invalidateCache, isCacheValid, isCodeFile, isEntryPoint, isExportedCloudFunction, isFirebaseProject, levenshteinDistance, map, readConfig, removeArea, setArea, setFileDescription, suggest, writeConfig };
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  AREA_DESCRIPTIONS,
3
3
  AREA_NAMES,
4
+ COMMAND_REFERENCE,
4
5
  FOLDER_PATTERNS,
5
6
  KEYWORD_PATTERNS,
6
7
  VERSION,
@@ -15,7 +16,14 @@ import {
15
16
  deadFix,
16
17
  detectCategory,
17
18
  detectFileAreas,
19
+ extractFileName,
18
20
  filterCloudFunctionsFalsePositives,
21
+ findBestMatch,
22
+ findSimilar,
23
+ formatAreaNotFound,
24
+ formatFileNotFound,
25
+ formatInvalidCommand,
26
+ formatMissingTarget,
19
27
  getAreaDescription,
20
28
  getAreaName,
21
29
  getCacheDir,
@@ -29,6 +37,7 @@ import {
29
37
  isEntryPoint,
30
38
  isExportedCloudFunction,
31
39
  isFirebaseProject,
40
+ levenshteinDistance,
32
41
  map,
33
42
  readConfig,
34
43
  removeArea,
@@ -36,10 +45,11 @@ import {
36
45
  setFileDescription,
37
46
  suggest,
38
47
  writeConfig
39
- } from "./chunk-A3QVUKVA.js";
48
+ } from "./chunk-PHTWZ2JT.js";
40
49
  export {
41
50
  AREA_DESCRIPTIONS,
42
51
  AREA_NAMES,
52
+ COMMAND_REFERENCE,
43
53
  FOLDER_PATTERNS,
44
54
  KEYWORD_PATTERNS,
45
55
  VERSION,
@@ -54,7 +64,14 @@ export {
54
64
  deadFix,
55
65
  detectCategory,
56
66
  detectFileAreas,
67
+ extractFileName,
57
68
  filterCloudFunctionsFalsePositives,
69
+ findBestMatch,
70
+ findSimilar,
71
+ formatAreaNotFound,
72
+ formatFileNotFound,
73
+ formatInvalidCommand,
74
+ formatMissingTarget,
58
75
  getAreaDescription,
59
76
  getAreaName,
60
77
  getCacheDir,
@@ -68,6 +85,7 @@ export {
68
85
  isEntryPoint,
69
86
  isExportedCloudFunction,
70
87
  isFirebaseProject,
88
+ levenshteinDistance,
71
89
  map,
72
90
  readConfig,
73
91
  removeArea,
@@ -8,7 +8,7 @@ import {
8
8
  impact,
9
9
  map,
10
10
  suggest
11
- } from "./chunk-A3QVUKVA.js";
11
+ } from "./chunk-PHTWZ2JT.js";
12
12
 
13
13
  // src/mcp/server.ts
14
14
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@justmpm/ai-tool",
3
- "version": "0.5.1",
3
+ "version": "0.5.4",
4
4
  "description": "Ferramenta de análise de dependências e impacto para projetos TypeScript/JavaScript. Usa Skott + Knip internamente.",
5
5
  "keywords": [
6
6
  "dependency-analysis",