@renseiai/agentfactory-code-intelligence 0.8.16 → 0.8.18

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.
@@ -5,9 +5,9 @@ describe('codeIntelligencePlugin', () => {
5
5
  expect(codeIntelligencePlugin.name).toBe('af-code-intelligence');
6
6
  expect(codeIntelligencePlugin.description).toBeTruthy();
7
7
  });
8
- it('creates 4 tools', () => {
8
+ it('creates 6 tools', () => {
9
9
  const tools = codeIntelligencePlugin.createTools({ env: {}, cwd: '/tmp' });
10
- expect(tools).toHaveLength(4);
10
+ expect(tools).toHaveLength(6);
11
11
  });
12
12
  it('creates tools with correct names', () => {
13
13
  const tools = codeIntelligencePlugin.createTools({ env: {}, cwd: '/tmp' });
@@ -16,6 +16,8 @@ describe('codeIntelligencePlugin', () => {
16
16
  expect(names).toContain('af_code_get_repo_map');
17
17
  expect(names).toContain('af_code_search_code');
18
18
  expect(names).toContain('af_code_check_duplicate');
19
+ expect(names).toContain('af_code_find_type_usages');
20
+ expect(names).toContain('af_code_validate_cross_deps');
19
21
  });
20
22
  it('tools have descriptions', () => {
21
23
  const tools = codeIntelligencePlugin.createTools({ env: {}, cwd: '/tmp' });
@@ -1 +1 @@
1
- {"version":3,"file":"code-intelligence-plugin.d.ts","sourceRoot":"","sources":["../../../src/plugin/code-intelligence-plugin.ts"],"names":[],"mappings":"AACA,OAAO,EAAQ,KAAK,oBAAoB,EAAE,MAAM,gCAAgC,CAAA;AAchF,mEAAmE;AACnE,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,CAAC,OAAO,EAAE,iBAAiB,GAAG,oBAAoB,CAAC,GAAG,CAAC,EAAE,CAAA;CACrE;AAED,qDAAqD;AACrD,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC3B,GAAG,EAAE,MAAM,CAAA;CACZ;AAED,2CAA2C;AAC3C,eAAO,MAAM,sBAAsB,EAAE,UAkHpC,CAAA"}
1
+ {"version":3,"file":"code-intelligence-plugin.d.ts","sourceRoot":"","sources":["../../../src/plugin/code-intelligence-plugin.ts"],"names":[],"mappings":"AAGA,OAAO,EAAQ,KAAK,oBAAoB,EAAE,MAAM,gCAAgC,CAAA;AAchF,mEAAmE;AACnE,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,CAAC,OAAO,EAAE,iBAAiB,GAAG,oBAAoB,CAAC,GAAG,CAAC,EAAE,CAAA;CACrE;AAED,qDAAqD;AACrD,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC3B,GAAG,EAAE,MAAM,CAAA;CACZ;AAiOD,2CAA2C;AAC3C,eAAO,MAAM,sBAAsB,EAAE,UA6JpC,CAAA"}
@@ -1,3 +1,5 @@
1
+ import { readFile, readdir, stat as fsStat } from 'node:fs/promises';
2
+ import { join, extname, relative } from 'node:path';
1
3
  import { z } from 'zod';
2
4
  import { tool } from '@anthropic-ai/claude-agent-sdk';
3
5
  import { SymbolExtractor } from '../parser/symbol-extractor.js';
@@ -7,6 +9,188 @@ import { HybridSearchEngine } from '../search/hybrid-search.js';
7
9
  import { RepoMapGenerator } from '../repo-map/repo-map-generator.js';
8
10
  import { DedupPipeline } from '../memory/dedup-pipeline.js';
9
11
  import { InMemoryStore } from '../memory/memory-store.js';
12
+ // ── Shared constants for file discovery ─────────────────────────────────────
13
+ const SUPPORTED_EXTENSIONS = new Set([
14
+ '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
15
+ '.py', '.go', '.rs',
16
+ ]);
17
+ const IGNORE_DIRS = new Set([
18
+ 'node_modules', 'dist', '.git', '.next', '.turbo',
19
+ 'build', 'coverage', '__pycache__', '.agentfactory',
20
+ '.worktrees', 'vendor', 'target',
21
+ ]);
22
+ async function discoverFiles(cwd) {
23
+ const files = new Map();
24
+ async function walk(dir) {
25
+ let entries;
26
+ try {
27
+ entries = await readdir(dir, { withFileTypes: true });
28
+ }
29
+ catch {
30
+ return;
31
+ }
32
+ for (const entry of entries) {
33
+ if (IGNORE_DIRS.has(entry.name))
34
+ continue;
35
+ const fullPath = join(dir, entry.name);
36
+ if (entry.isDirectory()) {
37
+ await walk(fullPath);
38
+ }
39
+ else if (entry.isFile() && SUPPORTED_EXTENSIONS.has(extname(entry.name))) {
40
+ try {
41
+ const s = await fsStat(fullPath);
42
+ if (s.size > 512 * 1024)
43
+ continue;
44
+ const content = await readFile(fullPath, 'utf-8');
45
+ files.set(relative(cwd, fullPath), content);
46
+ }
47
+ catch { /* skip */ }
48
+ }
49
+ }
50
+ }
51
+ await walk(cwd);
52
+ return files;
53
+ }
54
+ function escapeRegex(str) {
55
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
56
+ }
57
+ async function findTypeUsagesInProcess(cwd, typeName, maxResults) {
58
+ const files = await discoverFiles(cwd);
59
+ const usages = [];
60
+ const escaped = escapeRegex(typeName);
61
+ for (const [filePath, content] of files) {
62
+ if (!content.includes(typeName))
63
+ continue;
64
+ const lines = content.split('\n');
65
+ for (let i = 0; i < lines.length; i++) {
66
+ const line = lines[i];
67
+ if (line.match(/\bimport\b/) && line.includes(typeName)) {
68
+ usages.push({ filePath, line: i + 1, context: line.trim(), kind: 'import' });
69
+ continue;
70
+ }
71
+ if (/switch\s*\(/.test(line)) {
72
+ const windowEnd = Math.min(lines.length - 1, i + 50);
73
+ const window = lines.slice(i, windowEnd + 1).join('\n');
74
+ if (window.includes(typeName) || lines.slice(i, windowEnd + 1).some(l => /case\s+['"]/.test(l))) {
75
+ // Check if the switch variable is typed as our target
76
+ const switchWindow = lines.slice(Math.max(0, i - 5), i + 1).join('\n');
77
+ if (switchWindow.includes(typeName) || window.includes(typeName)) {
78
+ usages.push({ filePath, line: i + 1, context: line.trim(), kind: 'switch_case' });
79
+ }
80
+ }
81
+ }
82
+ if (new RegExp(`Record<\\s*${escaped}|satisfies\\s+Record<\\s*${escaped}`).test(line)) {
83
+ usages.push({ filePath, line: i + 1, context: line.trim(), kind: 'mapping_object' });
84
+ }
85
+ if ((line.includes('assertNever') || line.includes('exhaustive')) && content.includes(typeName)) {
86
+ usages.push({ filePath, line: i + 1, context: line.trim(), kind: 'exhaustive_check' });
87
+ }
88
+ if ((line.includes(`type ${typeName}`) || line.includes(`interface ${typeName}`) ||
89
+ new RegExp(`:\\s*${escaped}\\b`).test(line)) &&
90
+ !line.match(/\bimport\b/)) {
91
+ usages.push({ filePath, line: i + 1, context: line.trim(), kind: 'type_reference' });
92
+ }
93
+ }
94
+ }
95
+ const kindPriority = {
96
+ switch_case: 0, mapping_object: 1, exhaustive_check: 2, type_reference: 3, import: 4,
97
+ };
98
+ usages.sort((a, b) => (kindPriority[a.kind] ?? 5) - (kindPriority[b.kind] ?? 5));
99
+ return {
100
+ typeName,
101
+ totalUsages: usages.length,
102
+ usages: usages.slice(0, maxResults),
103
+ switchStatements: usages.filter(u => u.kind === 'switch_case').length,
104
+ mappingObjects: usages.filter(u => u.kind === 'mapping_object').length,
105
+ };
106
+ }
107
+ // ── In-process cross-package dep validator ──────────────────────────────────
108
+ async function validateCrossDepsInProcess(cwd, targetPath) {
109
+ const files = await discoverFiles(cwd);
110
+ // Discover workspace packages
111
+ const workspacePackages = new Map();
112
+ async function walkPkgs(dir, depth) {
113
+ if (depth > 5)
114
+ return;
115
+ let entries;
116
+ try {
117
+ entries = await readdir(dir, { withFileTypes: true });
118
+ }
119
+ catch {
120
+ return;
121
+ }
122
+ for (const entry of entries) {
123
+ if (IGNORE_DIRS.has(entry.name))
124
+ continue;
125
+ const fullPath = join(dir, entry.name);
126
+ if (entry.isDirectory()) {
127
+ await walkPkgs(fullPath, depth + 1);
128
+ }
129
+ else if (entry.name === 'package.json') {
130
+ try {
131
+ const content = JSON.parse(await readFile(fullPath, 'utf-8'));
132
+ if (content.name) {
133
+ const allDeps = new Set([
134
+ ...Object.keys(content.dependencies ?? {}),
135
+ ...Object.keys(content.devDependencies ?? {}),
136
+ ...Object.keys(content.peerDependencies ?? {}),
137
+ ]);
138
+ workspacePackages.set(relative(cwd, dir), { name: content.name, dir: relative(cwd, dir), deps: allDeps });
139
+ }
140
+ }
141
+ catch { /* skip */ }
142
+ }
143
+ }
144
+ }
145
+ await walkPkgs(cwd, 0);
146
+ function findOwningPkg(filePath) {
147
+ let best = null;
148
+ for (const [dir, pkg] of workspacePackages) {
149
+ if (filePath.startsWith(dir + '/') || filePath === dir) {
150
+ if (!best || dir.length > best.key.length)
151
+ best = { key: dir, pkg };
152
+ }
153
+ }
154
+ return best?.pkg;
155
+ }
156
+ const missingDeps = [];
157
+ let filesChecked = 0;
158
+ for (const [filePath, content] of files) {
159
+ if (targetPath && !filePath.startsWith(targetPath))
160
+ continue;
161
+ filesChecked++;
162
+ const owningPkg = findOwningPkg(filePath);
163
+ if (!owningPkg)
164
+ continue;
165
+ const lines = content.split('\n');
166
+ for (let i = 0; i < lines.length; i++) {
167
+ const importMatch = lines[i].match(/(?:from\s+['"]|require\s*\(\s*['"]|import\s+['"])(@[^'"\/]+\/[^'"\/]+|[^.'"\/@][^'"\/]*)/);
168
+ if (!importMatch)
169
+ continue;
170
+ const importedPkg = importMatch[1];
171
+ const isWorkspacePkg = [...workspacePackages.values()].some(wp => wp.name === importedPkg);
172
+ if (!isWorkspacePkg)
173
+ continue;
174
+ if (!owningPkg.deps.has(importedPkg)) {
175
+ missingDeps.push({
176
+ importingFile: filePath,
177
+ importedPackage: importedPkg,
178
+ packageJsonPath: join(owningPkg.dir, 'package.json'),
179
+ line: i + 1,
180
+ });
181
+ }
182
+ }
183
+ }
184
+ const seen = new Set();
185
+ const uniqueMissing = missingDeps.filter(d => {
186
+ const key = `${d.packageJsonPath}:${d.importedPackage}`;
187
+ if (seen.has(key))
188
+ return false;
189
+ seen.add(key);
190
+ return true;
191
+ });
192
+ return { valid: uniqueMissing.length === 0, missingDeps: uniqueMissing, packagesChecked: workspacePackages.size, filesChecked };
193
+ }
10
194
  /** Create the code intelligence plugin. */
11
195
  export const codeIntelligencePlugin = {
12
196
  name: 'af-code-intelligence',
@@ -99,6 +283,39 @@ export const codeIntelligencePlugin = {
99
283
  };
100
284
  }
101
285
  }),
286
+ tool('af_code_find_type_usages', 'Find all switch/case statements, mapping objects, and usage sites for a union type or enum. Use this before adding new members to a type to identify all files that need updating.', {
287
+ type_name: z.string().describe('The type/enum name to search for (e.g. "AgentWorkType")'),
288
+ max_results: z.number().optional().describe('Maximum results (default 50)'),
289
+ }, async (args) => {
290
+ try {
291
+ const result = await findTypeUsagesInProcess(context.cwd, args.type_name, args.max_results ?? 50);
292
+ return {
293
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
294
+ };
295
+ }
296
+ catch (err) {
297
+ return {
298
+ content: [{ type: 'text', text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
299
+ isError: true,
300
+ };
301
+ }
302
+ }),
303
+ tool('af_code_validate_cross_deps', 'Check that cross-package imports in a monorepo have corresponding package.json dependency declarations. Returns missing dependencies that would cause CI typecheck failures.', {
304
+ path: z.string().optional().describe('Optional directory/file to scope the check'),
305
+ }, async (args) => {
306
+ try {
307
+ const result = await validateCrossDepsInProcess(context.cwd, args.path);
308
+ return {
309
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
310
+ };
311
+ }
312
+ catch (err) {
313
+ return {
314
+ content: [{ type: 'text', text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
315
+ isError: true,
316
+ };
317
+ }
318
+ }),
102
319
  ];
103
320
  },
104
321
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@renseiai/agentfactory-code-intelligence",
3
- "version": "0.8.16",
3
+ "version": "0.8.18",
4
4
  "type": "module",
5
5
  "description": "Code intelligence for AgentFactory — tree-sitter AST parsing, BM25 search, incremental indexing, memory deduplication",
6
6
  "author": "Rensei AI (https://rensei.ai)",