@renseiai/agentfactory-cli 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.
@@ -13,6 +13,8 @@
13
13
  * get-repo-map Get PageRank-ranked repository file map
14
14
  * search-code <query> BM25/hybrid code search
15
15
  * check-duplicate Check content for duplicates
16
+ * find-type-usages <name> Find all switch/case, mapping, and usage sites for a type
17
+ * validate-cross-deps Check cross-package imports have package.json entries
16
18
  * help Show this help message
17
19
  *
18
20
  * Environment:
@@ -1 +1 @@
1
- {"version":3,"file":"code.d.ts","sourceRoot":"","sources":["../../src/code.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;GAmBG"}
1
+ {"version":3,"file":"code.d.ts","sourceRoot":"","sources":["../../src/code.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;GAqBG"}
package/dist/src/code.js CHANGED
@@ -13,6 +13,8 @@
13
13
  * get-repo-map Get PageRank-ranked repository file map
14
14
  * search-code <query> BM25/hybrid code search
15
15
  * check-duplicate Check content for duplicates
16
+ * find-type-usages <name> Find all switch/case, mapping, and usage sites for a type
17
+ * validate-cross-deps Check cross-package imports have package.json entries
16
18
  * help Show this help message
17
19
  *
18
20
  * Environment:
@@ -55,6 +57,12 @@ Options (check-duplicate):
55
57
  --content <string> Content to check (inline)
56
58
  --content-file <path> Path to file containing content to check
57
59
 
60
+ Options (find-type-usages):
61
+ --max-results <N> Maximum results (default: 50)
62
+
63
+ Options (validate-cross-deps):
64
+ [path] Optional directory/file to scope the check
65
+
58
66
  Index:
59
67
  First invocation builds the index from source files (~5-10s).
60
68
  Subsequent calls reuse the persisted index from .agentfactory/code-index/.
@@ -66,6 +74,9 @@ Examples:
66
74
  af-code search-code "incremental indexer" --language typescript
67
75
  af-code check-duplicate --content "function hello() { return 'world' }"
68
76
  af-code check-duplicate --content-file /tmp/snippet.ts
77
+ af-code find-type-usages "AgentWorkType"
78
+ af-code validate-cross-deps
79
+ af-code validate-cross-deps packages/linear
69
80
  `);
70
81
  }
71
82
  async function main() {
@@ -1 +1 @@
1
- {"version":3,"file":"code-intelligence-runner.d.ts","sourceRoot":"","sources":["../../../src/lib/code-intelligence-runner.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,4BAA4B;IAC3C,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC,CAAA;IACjD,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,GAAG,EAAE,MAAM,CAAA;CACZ;AAED,MAAM,WAAW,4BAA4B;IAC3C,MAAM,EAAE,OAAO,CAAA;CAChB;AAID,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG;IAC7C,OAAO,EAAE,MAAM,GAAG,SAAS,CAAA;IAC3B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC,CAAA;IACjD,cAAc,EAAE,MAAM,EAAE,CAAA;CACzB,CAwBA;AAgLD,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,4BAA4B,GACnC,OAAO,CAAC,4BAA4B,CAAC,CAevC"}
1
+ {"version":3,"file":"code-intelligence-runner.d.ts","sourceRoot":"","sources":["../../../src/lib/code-intelligence-runner.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,4BAA4B;IAC3C,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC,CAAA;IACjD,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,GAAG,EAAE,MAAM,CAAA;CACZ;AAED,MAAM,WAAW,4BAA4B;IAC3C,MAAM,EAAE,OAAO,CAAA;CAChB;AAID,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG;IAC7C,OAAO,EAAE,MAAM,GAAG,SAAS,CAAA;IAC3B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC,CAAA;IACjD,cAAc,EAAE,MAAM,EAAE,CAAA;CACzB,CAwBA;AA0aD,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,4BAA4B,GACnC,OAAO,CAAC,4BAA4B,CAAC,CAuBvC"}
@@ -171,8 +171,210 @@ async function checkDuplicate(config, engines) {
171
171
  const result = await engines.dedupPipeline.check(content);
172
172
  return result;
173
173
  }
174
+ async function findTypeUsages(config) {
175
+ const typeName = config.positionalArgs[0];
176
+ if (!typeName)
177
+ throw new Error('Usage: af-code find-type-usages <TypeName>');
178
+ const maxResults = config.args['max-results'] ? Number(config.args['max-results']) : 50;
179
+ const files = await discoverSourceFiles(config.cwd);
180
+ const usages = [];
181
+ // Patterns that indicate exhaustive switch/case, mapping objects, or type references
182
+ const switchPattern = new RegExp(`switch\\s*\\(`, 'g');
183
+ const casePattern = new RegExp(`case\\s+['"]`, 'g');
184
+ const importPattern = new RegExp(`\\b${escapeRegex(typeName)}\\b`, 'g');
185
+ const mappingPattern = new RegExp(`(?:Record<\\s*${escapeRegex(typeName)}|:\\s*\\{\\s*\\[\\w+\\s+in\\s+${escapeRegex(typeName)}\\]|satisfies\\s+Record<\\s*${escapeRegex(typeName)})`, 'g');
186
+ for (const [filePath, content] of files) {
187
+ if (!content.includes(typeName))
188
+ continue;
189
+ const lines = content.split('\n');
190
+ for (let i = 0; i < lines.length; i++) {
191
+ const line = lines[i];
192
+ // Check for import of the type
193
+ if (line.match(/\bimport\b/) && line.includes(typeName)) {
194
+ usages.push({ filePath, line: i + 1, context: line.trim(), kind: 'import' });
195
+ continue;
196
+ }
197
+ // Check for switch statements — look for switch keyword near the type name usage
198
+ if (switchPattern.test(line)) {
199
+ // Scan surrounding lines for the type name
200
+ const windowStart = Math.max(0, i - 2);
201
+ const windowEnd = Math.min(lines.length - 1, i + 5);
202
+ const window = lines.slice(windowStart, windowEnd + 1).join('\n');
203
+ if (window.includes(typeName) || hasRelatedCases(lines, i, typeName)) {
204
+ usages.push({ filePath, line: i + 1, context: line.trim(), kind: 'switch_case' });
205
+ }
206
+ switchPattern.lastIndex = 0;
207
+ }
208
+ // Check for Record<TypeName, ...> or mapping objects
209
+ if (mappingPattern.test(line)) {
210
+ usages.push({ filePath, line: i + 1, context: line.trim(), kind: 'mapping_object' });
211
+ mappingPattern.lastIndex = 0;
212
+ }
213
+ // Check for exhaustive checks (assertNever, default: throw, etc.)
214
+ if ((line.includes('assertNever') || line.includes('exhaustive')) &&
215
+ content.includes(typeName)) {
216
+ usages.push({ filePath, line: i + 1, context: line.trim(), kind: 'exhaustive_check' });
217
+ }
218
+ // Check for type definition/reference (union type definitions, extends, etc.)
219
+ if ((line.includes(`type ${typeName}`) ||
220
+ line.includes(`interface ${typeName}`) ||
221
+ line.match(new RegExp(`:\\s*${escapeRegex(typeName)}\\b`))) &&
222
+ !line.match(/\bimport\b/)) {
223
+ usages.push({ filePath, line: i + 1, context: line.trim(), kind: 'type_reference' });
224
+ }
225
+ }
226
+ }
227
+ // Deduplicate and sort: switch_case and mapping_object first (most actionable)
228
+ const kindPriority = {
229
+ switch_case: 0,
230
+ mapping_object: 1,
231
+ exhaustive_check: 2,
232
+ type_reference: 3,
233
+ import: 4,
234
+ };
235
+ usages.sort((a, b) => (kindPriority[a.kind] ?? 5) - (kindPriority[b.kind] ?? 5));
236
+ return {
237
+ typeName,
238
+ totalUsages: usages.length,
239
+ usages: usages.slice(0, maxResults),
240
+ switchStatements: usages.filter(u => u.kind === 'switch_case').length,
241
+ mappingObjects: usages.filter(u => u.kind === 'mapping_object').length,
242
+ };
243
+ }
244
+ /** Check if a switch block's case statements relate to a union type */
245
+ function hasRelatedCases(lines, switchLine, _typeName) {
246
+ // Scan forward from the switch line looking for string literal cases
247
+ for (let j = switchLine; j < Math.min(lines.length, switchLine + 50); j++) {
248
+ if (lines[j].includes('case \'') || lines[j].includes('case "')) {
249
+ return true; // Has string literal cases, likely a discriminated union switch
250
+ }
251
+ if (lines[j].match(/^\s*\}/))
252
+ break; // End of block
253
+ }
254
+ return false;
255
+ }
256
+ function escapeRegex(str) {
257
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
258
+ }
259
+ async function validateCrossDeps(config) {
260
+ const targetPath = config.positionalArgs[0]; // Optional: specific file or directory
261
+ const files = await discoverSourceFiles(config.cwd);
262
+ // 1. Build a map of workspace packages by reading all package.json files
263
+ const workspacePackages = new Map();
264
+ await discoverWorkspacePackages(config.cwd, workspacePackages);
265
+ // 2. Map file paths to their owning workspace package
266
+ function findOwningPackage(filePath) {
267
+ let bestMatch = null;
268
+ for (const [dir, pkg] of workspacePackages) {
269
+ if (filePath.startsWith(dir + '/') || filePath === dir) {
270
+ if (!bestMatch || dir.length > bestMatch.key.length) {
271
+ bestMatch = { key: dir, pkg };
272
+ }
273
+ }
274
+ }
275
+ return bestMatch?.pkg;
276
+ }
277
+ const missingDeps = [];
278
+ // 3. Check each file for cross-package imports
279
+ for (const [filePath, content] of files) {
280
+ if (targetPath && !filePath.startsWith(targetPath))
281
+ continue;
282
+ const owningPkg = findOwningPackage(filePath);
283
+ if (!owningPkg)
284
+ continue;
285
+ const lines = content.split('\n');
286
+ for (let i = 0; i < lines.length; i++) {
287
+ const line = lines[i];
288
+ // Match import/require of workspace packages
289
+ const importMatch = line.match(/(?:from\s+['"]|require\s*\(\s*['"]|import\s+['"])(@[^'"\/]+\/[^'"\/]+|[^.'"\/@][^'"\/]*)/);
290
+ if (!importMatch)
291
+ continue;
292
+ const importedPkg = importMatch[1];
293
+ // Check if this is a workspace package
294
+ const isWorkspacePkg = [...workspacePackages.values()].some(wp => wp.name === importedPkg);
295
+ if (!isWorkspacePkg)
296
+ continue;
297
+ // Check if it's declared in package.json
298
+ if (!owningPkg.deps.has(importedPkg)) {
299
+ missingDeps.push({
300
+ importingFile: filePath,
301
+ importedPackage: importedPkg,
302
+ packageJsonPath: join(owningPkg.dir, 'package.json'),
303
+ line: i + 1,
304
+ });
305
+ }
306
+ }
307
+ }
308
+ // Deduplicate by (packageJsonPath, importedPackage)
309
+ const seen = new Set();
310
+ const uniqueMissing = missingDeps.filter(d => {
311
+ const key = `${d.packageJsonPath}:${d.importedPackage}`;
312
+ if (seen.has(key))
313
+ return false;
314
+ seen.add(key);
315
+ return true;
316
+ });
317
+ return {
318
+ valid: uniqueMissing.length === 0,
319
+ missingDeps: uniqueMissing,
320
+ packagesChecked: workspacePackages.size,
321
+ filesChecked: targetPath
322
+ ? [...files.keys()].filter(f => f.startsWith(targetPath)).length
323
+ : files.size,
324
+ };
325
+ }
326
+ async function discoverWorkspacePackages(cwd, result) {
327
+ // Find all package.json files in the workspace (skip node_modules, dist)
328
+ async function walk(dir, depth) {
329
+ if (depth > 5)
330
+ return;
331
+ let entries;
332
+ try {
333
+ entries = await readdir(dir, { withFileTypes: true });
334
+ }
335
+ catch {
336
+ return;
337
+ }
338
+ for (const entry of entries) {
339
+ if (IGNORE_DIRS.has(entry.name))
340
+ continue;
341
+ const fullPath = join(dir, entry.name);
342
+ if (entry.isDirectory()) {
343
+ await walk(fullPath, depth + 1);
344
+ }
345
+ else if (entry.name === 'package.json') {
346
+ try {
347
+ const content = JSON.parse(await readFile(fullPath, 'utf-8'));
348
+ if (content.name) {
349
+ const allDeps = new Set([
350
+ ...Object.keys(content.dependencies ?? {}),
351
+ ...Object.keys(content.devDependencies ?? {}),
352
+ ...Object.keys(content.peerDependencies ?? {}),
353
+ ]);
354
+ result.set(relative(cwd, dir), {
355
+ name: content.name,
356
+ dir: relative(cwd, dir),
357
+ deps: allDeps,
358
+ });
359
+ }
360
+ }
361
+ catch {
362
+ // Skip malformed package.json
363
+ }
364
+ }
365
+ }
366
+ }
367
+ await walk(cwd, 0);
368
+ }
174
369
  // ── Main runner ─────────────────────────────────────────────────────────────
175
370
  export async function runCodeIntelligence(config) {
371
+ // Commands that don't need the full index
372
+ switch (config.command) {
373
+ case 'find-type-usages':
374
+ return { output: await findTypeUsages(config) };
375
+ case 'validate-cross-deps':
376
+ return { output: await validateCrossDeps(config) };
377
+ }
176
378
  const engines = await initializeIndex(config.cwd);
177
379
  switch (config.command) {
178
380
  case 'search-symbols':
@@ -184,6 +386,6 @@ export async function runCodeIntelligence(config) {
184
386
  case 'check-duplicate':
185
387
  return { output: await checkDuplicate(config, engines) };
186
388
  default:
187
- throw new Error(`Unknown command: ${config.command}. Available: search-symbols, get-repo-map, search-code, check-duplicate`);
389
+ throw new Error(`Unknown command: ${config.command}. Available: search-symbols, get-repo-map, search-code, check-duplicate, find-type-usages, validate-cross-deps`);
188
390
  }
189
391
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@renseiai/agentfactory-cli",
3
- "version": "0.8.16",
3
+ "version": "0.8.18",
4
4
  "type": "module",
5
5
  "description": "CLI tools for AgentFactory — local orchestrator, remote worker, queue admin",
6
6
  "author": "Rensei AI (https://rensei.ai)",
@@ -126,12 +126,12 @@
126
126
  ],
127
127
  "dependencies": {
128
128
  "dotenv": "^17.2.3",
129
- "@renseiai/agentfactory": "0.8.16",
130
- "@renseiai/agentfactory-server": "0.8.16",
131
- "@renseiai/plugin-linear": "0.8.16"
129
+ "@renseiai/plugin-linear": "0.8.18",
130
+ "@renseiai/agentfactory-server": "0.8.18",
131
+ "@renseiai/agentfactory": "0.8.18"
132
132
  },
133
133
  "optionalDependencies": {
134
- "@renseiai/agentfactory-code-intelligence": "0.8.16"
134
+ "@renseiai/agentfactory-code-intelligence": "0.8.18"
135
135
  },
136
136
  "devDependencies": {
137
137
  "@types/node": "^22.5.4",