@renseiai/agentfactory-cli 0.8.17 → 0.8.19

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;AA+gBD,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,4BAA4B,GACnC,OAAO,CAAC,4BAA4B,CAAC,CAuBvC"}
@@ -171,8 +171,290 @@ 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
+ /**
260
+ * Determine whether a line contains a real import statement (not one inside
261
+ * a comment, string literal, template literal, or test assertion).
262
+ *
263
+ * The caller must maintain the parse state across lines.
264
+ */
265
+ function isRealImportLine(line, state) {
266
+ const trimmed = line.trim();
267
+ // Track block comment boundaries
268
+ if (state.inBlockComment) {
269
+ if (trimmed.includes('*/')) {
270
+ return { real: false, state: { ...state, inBlockComment: false } };
271
+ }
272
+ return { real: false, state };
273
+ }
274
+ if (trimmed.startsWith('/*')) {
275
+ const closesOnSameLine = trimmed.includes('*/');
276
+ return { real: false, state: { ...state, inBlockComment: !closesOnSameLine } };
277
+ }
278
+ // Track template literal boundaries (backtick strings spanning multiple lines)
279
+ // Count unescaped backticks to toggle state
280
+ if (state.inTemplateLiteral) {
281
+ const backtickCount = countUnescapedBackticks(line);
282
+ if (backtickCount % 2 === 1) {
283
+ // Odd number of backticks — template literal ends on this line
284
+ return { real: false, state: { ...state, inTemplateLiteral: false } };
285
+ }
286
+ return { real: false, state };
287
+ }
288
+ // Single-line comments and JSDoc continuation lines
289
+ if (trimmed.startsWith('//') || trimmed.startsWith('*'))
290
+ return { real: false, state };
291
+ // Check if this line opens a template literal that doesn't close on the same line
292
+ const backticks = countUnescapedBackticks(line);
293
+ if (backticks % 2 === 1) {
294
+ // Odd backticks — a template literal opens and doesn't close on this line.
295
+ // The import on this line (if any) could be before or after the backtick.
296
+ // If the import keyword appears after a backtick, it's template content.
297
+ const importIdx = line.search(/\b(import|export)\s/);
298
+ const firstBacktick = line.indexOf('`');
299
+ if (importIdx >= 0 && firstBacktick >= 0 && importIdx > firstBacktick) {
300
+ // Import appears inside the template literal
301
+ return { real: false, state: { ...state, inTemplateLiteral: true } };
302
+ }
303
+ // Import appears before the backtick — real import, but template opens
304
+ if (importIdx >= 0 && (firstBacktick < 0 || importIdx < firstBacktick)) {
305
+ return { real: true, state: { ...state, inTemplateLiteral: true } };
306
+ }
307
+ return { real: false, state: { ...state, inTemplateLiteral: true } };
308
+ }
309
+ // Real import/export/require statements start at the beginning of the line
310
+ if (/^\s*(import|export)\s/.test(line))
311
+ return { real: true, state };
312
+ // Dynamic require: `const x = require('pkg')`
313
+ if (/\brequire\s*\(/.test(line)) {
314
+ const reqIdx = line.indexOf('require');
315
+ const beforeReq = line.slice(0, reqIdx);
316
+ if (beforeReq.includes('`') || beforeReq.includes("'require") || beforeReq.includes('"require')) {
317
+ return { real: false, state };
318
+ }
319
+ return { real: true, state };
320
+ }
321
+ return { real: false, state };
322
+ }
323
+ /** Count unescaped backticks in a line */
324
+ function countUnescapedBackticks(line) {
325
+ let count = 0;
326
+ for (let i = 0; i < line.length; i++) {
327
+ if (line[i] === '`' && (i === 0 || line[i - 1] !== '\\')) {
328
+ count++;
329
+ }
330
+ }
331
+ return count;
332
+ }
333
+ async function validateCrossDeps(config) {
334
+ const targetPath = config.positionalArgs[0]; // Optional: specific file or directory
335
+ const files = await discoverSourceFiles(config.cwd);
336
+ // 1. Build a map of workspace packages by reading all package.json files
337
+ const workspacePackages = new Map();
338
+ await discoverWorkspacePackages(config.cwd, workspacePackages);
339
+ // 2. Map file paths to their owning workspace package
340
+ function findOwningPackage(filePath) {
341
+ let bestMatch = null;
342
+ for (const [dir, pkg] of workspacePackages) {
343
+ if (filePath.startsWith(dir + '/') || filePath === dir) {
344
+ if (!bestMatch || dir.length > bestMatch.key.length) {
345
+ bestMatch = { key: dir, pkg };
346
+ }
347
+ }
348
+ }
349
+ return bestMatch?.pkg;
350
+ }
351
+ const missingDeps = [];
352
+ // 3. Check each file for cross-package imports
353
+ for (const [filePath, content] of files) {
354
+ if (targetPath && !filePath.startsWith(targetPath))
355
+ continue;
356
+ const owningPkg = findOwningPackage(filePath);
357
+ if (!owningPkg)
358
+ continue;
359
+ const lines = content.split('\n');
360
+ let parseState = { inBlockComment: false, inTemplateLiteral: false };
361
+ for (let i = 0; i < lines.length; i++) {
362
+ const line = lines[i];
363
+ // Skip comments, string literals, and template content
364
+ const classification = isRealImportLine(line, parseState);
365
+ parseState = classification.state;
366
+ if (!classification.real)
367
+ continue;
368
+ // Match import/require of workspace packages
369
+ const importMatch = line.match(/(?:from\s+['"]|require\s*\(\s*['"]|import\s+['"])(@[^'"\/]+\/[^'"\/]+|[^.'"\/@][^'"\/]*)/);
370
+ if (!importMatch)
371
+ continue;
372
+ const importedPkg = importMatch[1];
373
+ // Check if this is a workspace package
374
+ const isWorkspacePkg = [...workspacePackages.values()].some(wp => wp.name === importedPkg);
375
+ if (!isWorkspacePkg)
376
+ continue;
377
+ // Check if it's declared in package.json
378
+ if (!owningPkg.deps.has(importedPkg)) {
379
+ missingDeps.push({
380
+ importingFile: filePath,
381
+ importedPackage: importedPkg,
382
+ packageJsonPath: join(owningPkg.dir, 'package.json'),
383
+ line: i + 1,
384
+ });
385
+ }
386
+ }
387
+ }
388
+ // Deduplicate by (packageJsonPath, importedPackage)
389
+ const seen = new Set();
390
+ const uniqueMissing = missingDeps.filter(d => {
391
+ const key = `${d.packageJsonPath}:${d.importedPackage}`;
392
+ if (seen.has(key))
393
+ return false;
394
+ seen.add(key);
395
+ return true;
396
+ });
397
+ return {
398
+ valid: uniqueMissing.length === 0,
399
+ missingDeps: uniqueMissing,
400
+ packagesChecked: workspacePackages.size,
401
+ filesChecked: targetPath
402
+ ? [...files.keys()].filter(f => f.startsWith(targetPath)).length
403
+ : files.size,
404
+ };
405
+ }
406
+ async function discoverWorkspacePackages(cwd, result) {
407
+ // Find all package.json files in the workspace (skip node_modules, dist)
408
+ async function walk(dir, depth) {
409
+ if (depth > 5)
410
+ return;
411
+ let entries;
412
+ try {
413
+ entries = await readdir(dir, { withFileTypes: true });
414
+ }
415
+ catch {
416
+ return;
417
+ }
418
+ for (const entry of entries) {
419
+ if (IGNORE_DIRS.has(entry.name))
420
+ continue;
421
+ const fullPath = join(dir, entry.name);
422
+ if (entry.isDirectory()) {
423
+ await walk(fullPath, depth + 1);
424
+ }
425
+ else if (entry.name === 'package.json') {
426
+ try {
427
+ const content = JSON.parse(await readFile(fullPath, 'utf-8'));
428
+ if (content.name) {
429
+ const allDeps = new Set([
430
+ ...Object.keys(content.dependencies ?? {}),
431
+ ...Object.keys(content.devDependencies ?? {}),
432
+ ...Object.keys(content.peerDependencies ?? {}),
433
+ ]);
434
+ result.set(relative(cwd, dir), {
435
+ name: content.name,
436
+ dir: relative(cwd, dir),
437
+ deps: allDeps,
438
+ });
439
+ }
440
+ }
441
+ catch {
442
+ // Skip malformed package.json
443
+ }
444
+ }
445
+ }
446
+ }
447
+ await walk(cwd, 0);
448
+ }
174
449
  // ── Main runner ─────────────────────────────────────────────────────────────
175
450
  export async function runCodeIntelligence(config) {
451
+ // Commands that don't need the full index
452
+ switch (config.command) {
453
+ case 'find-type-usages':
454
+ return { output: await findTypeUsages(config) };
455
+ case 'validate-cross-deps':
456
+ return { output: await validateCrossDeps(config) };
457
+ }
176
458
  const engines = await initializeIndex(config.cwd);
177
459
  switch (config.command) {
178
460
  case 'search-symbols':
@@ -184,6 +466,6 @@ export async function runCodeIntelligence(config) {
184
466
  case 'check-duplicate':
185
467
  return { output: await checkDuplicate(config, engines) };
186
468
  default:
187
- throw new Error(`Unknown command: ${config.command}. Available: search-symbols, get-repo-map, search-code, check-duplicate`);
469
+ throw new Error(`Unknown command: ${config.command}. Available: search-symbols, get-repo-map, search-code, check-duplicate, find-type-usages, validate-cross-deps`);
188
470
  }
189
471
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@renseiai/agentfactory-cli",
3
- "version": "0.8.17",
3
+ "version": "0.8.19",
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.17",
130
- "@renseiai/plugin-linear": "0.8.17",
131
- "@renseiai/agentfactory-server": "0.8.17"
129
+ "@renseiai/plugin-linear": "0.8.19",
130
+ "@renseiai/agentfactory-server": "0.8.19",
131
+ "@renseiai/agentfactory": "0.8.19"
132
132
  },
133
133
  "optionalDependencies": {
134
- "@renseiai/agentfactory-code-intelligence": "0.8.17"
134
+ "@renseiai/agentfactory-code-intelligence": "0.8.19"
135
135
  },
136
136
  "devDependencies": {
137
137
  "@types/node": "^22.5.4",