gitnexus 1.6.6-rc.83 → 1.6.6-rc.85

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.
@@ -1,5 +1,5 @@
1
1
  import type { HttpLanguagePlugin } from './types.js';
2
- export type { HttpDetection, HttpLanguagePlugin, HttpRole } from './types.js';
2
+ export type { HttpDetection, HttpFileDetections, HttpLanguagePlugin, HttpRole, HttpScanInput, } from './types.js';
3
3
  /**
4
4
  * Glob for files worth scanning for HTTP routes. Kept alongside the
5
5
  * registry so adding a new language widens the glob in one edit.
@@ -22,45 +22,64 @@ const METHOD_ANNOTATION_TO_HTTP = {
22
22
  DeleteMapping: 'DELETE',
23
23
  PatchMapping: 'PATCH',
24
24
  };
25
- // ─── Provider: Spring class-level @RequestMapping prefix ──────────────
26
- // Two patterns are needed because the AST shape differs depending on
27
- // whether the annotation uses a positional argument or a named one:
28
- // @RequestMapping("/api") → (annotation_argument_list (string_literal))
29
- // @RequestMapping(path = "/api") → (annotation_argument_list (element_value_pair key:(identifier) value:(string_literal)))
30
- // @RequestMapping(value = "/api") → same as above
31
- //
32
- // The named-argument pattern MUST constrain the `key` field to the route
33
- // member names (`path`/`value`); without it, the query also captures
34
- // non-route attributes such as `produces`, `consumes`, `headers`, `name`,
35
- // `params` (their right-hand string literals would be mis-extracted as
36
- // route prefixes — e.g. `produces = "application/json"` would corrupt
37
- // every method route under that controller). The sibling
38
- // `topic-patterns/java.ts` uses the same `key:` constraint approach.
39
- const SPRING_CLASS_PREFIX_PATTERNS = compilePatterns({
40
- name: 'java-spring-class-prefix',
25
+ // ─── Provider: Spring class/interface-level @RequestMapping prefix ───
26
+ const SPRING_TYPE_PREFIX_PATTERNS = compilePatterns({
27
+ name: 'java-spring-type-prefix',
41
28
  language: Java,
42
29
  patterns: [
43
30
  {
44
31
  meta: {},
45
32
  query: `
46
- (class_declaration
47
- (modifiers
48
- (annotation
49
- name: (identifier) @ann (#eq? @ann "RequestMapping")
50
- arguments: (annotation_argument_list (string_literal) @prefix)))) @class
33
+ [
34
+ (class_declaration
35
+ (modifiers
36
+ (annotation
37
+ name: (identifier) @ann (#eq? @ann "RequestMapping")
38
+ arguments: (annotation_argument_list (string_literal) @prefix)))) @type
39
+ (interface_declaration
40
+ (modifiers
41
+ (annotation
42
+ name: (identifier) @ann (#eq? @ann "RequestMapping")
43
+ arguments: (annotation_argument_list (string_literal) @prefix)))) @type
44
+ ]
51
45
  `,
52
46
  },
53
47
  {
54
48
  meta: {},
55
49
  query: `
56
- (class_declaration
57
- (modifiers
58
- (annotation
59
- name: (identifier) @ann (#eq? @ann "RequestMapping")
60
- arguments: (annotation_argument_list
61
- (element_value_pair
62
- key: (identifier) @key (#match? @key "^(path|value)$")
63
- value: (string_literal) @prefix))))) @class
50
+ [
51
+ (class_declaration
52
+ (modifiers
53
+ (annotation
54
+ name: (identifier) @ann (#eq? @ann "RequestMapping")
55
+ arguments: (annotation_argument_list
56
+ (element_value_pair
57
+ key: (identifier) @key (#match? @key "^(path|value)$")
58
+ value: (string_literal) @prefix))))) @type
59
+ (interface_declaration
60
+ (modifiers
61
+ (annotation
62
+ name: (identifier) @ann (#eq? @ann "RequestMapping")
63
+ arguments: (annotation_argument_list
64
+ (element_value_pair
65
+ key: (identifier) @key (#match? @key "^(path|value)$")
66
+ value: (string_literal) @prefix))))) @type
67
+ ]
68
+ `,
69
+ },
70
+ ],
71
+ });
72
+ const SPRING_TYPE_DECLARATION_PATTERNS = compilePatterns({
73
+ name: 'java-spring-type-declaration',
74
+ language: Java,
75
+ patterns: [
76
+ {
77
+ meta: {},
78
+ query: `
79
+ [
80
+ (class_declaration name: (identifier) @type_name) @type
81
+ (interface_declaration name: (identifier) @type_name) @type
82
+ ]
64
83
  `,
65
84
  },
66
85
  ],
@@ -289,9 +308,9 @@ const APACHE_HTTP_CLIENT_PATTERNS = compilePatterns({
289
308
  ],
290
309
  });
291
310
  /**
292
- * Find the nearest enclosing class_declaration ancestor for a node, or
293
- * null if the node is top-level. Tree-sitter's SyntaxNode.parent walks
294
- * one level at a time.
311
+ * Find the nearest enclosing class/interface declaration ancestor for
312
+ * a node, or null if the node is top-level. Tree-sitter's
313
+ * SyntaxNode.parent walks one level at a time.
295
314
  */
296
315
  function findEnclosingClass(node) {
297
316
  let cur = node.parent;
@@ -311,23 +330,6 @@ function findEnclosingInterface(node) {
311
330
  }
312
331
  return null;
313
332
  }
314
- function hasAnnotation(node, annotationName) {
315
- for (const child of node.namedChildren) {
316
- if (child.type !== 'modifiers')
317
- continue;
318
- for (const modifier of child.namedChildren) {
319
- if (modifier.type !== 'annotation')
320
- continue;
321
- const nameNode = modifier.childForFieldName('name');
322
- if (!nameNode)
323
- continue;
324
- const simpleName = nameNode.text.split('.').pop();
325
- if (nameNode.text === annotationName || simpleName === annotationName)
326
- return true;
327
- }
328
- }
329
- return false;
330
- }
331
333
  /**
332
334
  * Join a class-level prefix and a method-level path into a single URL
333
335
  * path. Mirrors the semantics of the original regex implementation:
@@ -341,22 +343,186 @@ function joinPath(prefix, methodPath) {
341
343
  return `/${cleanSub}`;
342
344
  return `/${cleanPrefix}/${cleanSub}`;
343
345
  }
346
+ function getNodeName(node) {
347
+ return node.childForFieldName('name')?.text ?? null;
348
+ }
349
+ function hasAnnotation(node, names) {
350
+ const modifiers = node.namedChildren.find((child) => child.type === 'modifiers');
351
+ if (!modifiers)
352
+ return false;
353
+ const allowed = new Set(typeof names === 'string' ? [names] : names);
354
+ const stack = [...modifiers.namedChildren];
355
+ while (stack.length > 0) {
356
+ const cur = stack.pop();
357
+ const annotationName = cur.childForFieldName('name')?.text ?? '';
358
+ const simpleName = annotationName.split('.').pop() ?? annotationName;
359
+ if ((cur.type === 'annotation' || cur.type === 'marker_annotation') &&
360
+ (allowed.has(annotationName) || allowed.has(simpleName))) {
361
+ return true;
362
+ }
363
+ stack.push(...cur.namedChildren);
364
+ }
365
+ return false;
366
+ }
367
+ function collectTypePrefixes(tree) {
368
+ const prefixByTypeId = new Map();
369
+ for (const match of runCompiledPatterns(SPRING_TYPE_PREFIX_PATTERNS, tree)) {
370
+ const prefixNode = match.captures.prefix;
371
+ const typeNode = match.captures.type;
372
+ if (!prefixNode || !typeNode)
373
+ continue;
374
+ const prefix = unquoteLiteral(prefixNode.text);
375
+ if (prefix !== null)
376
+ prefixByTypeId.set(typeNode.id, prefix);
377
+ }
378
+ return prefixByTypeId;
379
+ }
380
+ function collectMethodRoutes(tree) {
381
+ const routesByMethodId = new Map();
382
+ for (const match of runCompiledPatterns(SPRING_METHOD_ROUTE_PATTERNS, tree)) {
383
+ const annNode = match.captures.ann;
384
+ const pathNode = match.captures.path;
385
+ const methodNode = match.captures.method;
386
+ if (!annNode || !pathNode || !methodNode)
387
+ continue;
388
+ const httpMethod = METHOD_ANNOTATION_TO_HTTP[annNode.text];
389
+ if (!httpMethod)
390
+ continue;
391
+ const rawPath = unquoteLiteral(pathNode.text);
392
+ if (rawPath === null)
393
+ continue;
394
+ const routes = routesByMethodId.get(methodNode.id) ?? [];
395
+ routes.push({ method: httpMethod, path: rawPath });
396
+ routesByMethodId.set(methodNode.id, routes);
397
+ }
398
+ return routesByMethodId;
399
+ }
400
+ function collectDirectMethods(typeNode) {
401
+ const out = [];
402
+ const visit = (node) => {
403
+ for (const child of node.namedChildren) {
404
+ if (child.type === 'method_declaration') {
405
+ out.push(child);
406
+ continue;
407
+ }
408
+ if (child !== typeNode &&
409
+ (child.type === 'class_declaration' || child.type === 'interface_declaration')) {
410
+ continue;
411
+ }
412
+ visit(child);
413
+ }
414
+ };
415
+ visit(typeNode);
416
+ return out;
417
+ }
418
+ function collectImplementedInterfaces(typeNode) {
419
+ const interfacesNode = typeNode.childForFieldName('interfaces');
420
+ if (!interfacesNode)
421
+ return [];
422
+ const out = [];
423
+ const visit = (node) => {
424
+ if (node.type === 'type_identifier' || node.type === 'scoped_type_identifier') {
425
+ out.push(node.text.split('.').pop() ?? node.text);
426
+ return;
427
+ }
428
+ for (const child of node.namedChildren)
429
+ visit(child);
430
+ };
431
+ visit(interfacesNode);
432
+ return out;
433
+ }
434
+ function collectSpringTypes(filePath, tree) {
435
+ const prefixByTypeId = collectTypePrefixes(tree);
436
+ const routesByMethodId = collectMethodRoutes(tree);
437
+ const out = [];
438
+ for (const match of runCompiledPatterns(SPRING_TYPE_DECLARATION_PATTERNS, tree)) {
439
+ const typeNode = match.captures.type;
440
+ const typeNameNode = match.captures.type_name;
441
+ if (!typeNode || !typeNameNode)
442
+ continue;
443
+ const kind = typeNode.type === 'interface_declaration' ? 'interface' : 'class';
444
+ const methods = collectDirectMethods(typeNode)
445
+ .map((methodNode) => ({
446
+ name: getNodeName(methodNode),
447
+ routes: routesByMethodId.get(methodNode.id) ?? [],
448
+ }))
449
+ .filter((method) => method.name !== null);
450
+ out.push({
451
+ filePath,
452
+ kind,
453
+ name: typeNameNode.text,
454
+ classPrefix: prefixByTypeId.get(typeNode.id) ?? '',
455
+ implementedInterfaces: kind === 'class' ? collectImplementedInterfaces(typeNode) : [],
456
+ isController: kind === 'class' && hasAnnotation(typeNode, ['RestController', 'Controller']),
457
+ methods,
458
+ });
459
+ }
460
+ return out;
461
+ }
462
+ function scanSpringProject(files) {
463
+ const types = files.flatMap((file) => collectSpringTypes(file.filePath, file.tree));
464
+ const interfaceRoutes = new Map();
465
+ for (const type of types) {
466
+ if (type.kind !== 'interface')
467
+ continue;
468
+ if (interfaceRoutes.has(type.name)) {
469
+ interfaceRoutes.set(type.name, null);
470
+ continue;
471
+ }
472
+ const methodMap = new Map();
473
+ for (const method of type.methods) {
474
+ const routes = method.routes.map((route) => ({
475
+ method: route.method,
476
+ path: type.classPrefix ? joinPath(type.classPrefix, route.path) : route.path,
477
+ }));
478
+ if (routes.length > 0)
479
+ methodMap.set(method.name, routes);
480
+ }
481
+ interfaceRoutes.set(type.name, methodMap);
482
+ }
483
+ const detectionsByFile = new Map();
484
+ for (const type of types) {
485
+ if (type.kind !== 'class' || !type.isController)
486
+ continue;
487
+ for (const method of type.methods) {
488
+ if (method.routes.length > 0)
489
+ continue;
490
+ const inheritedRoutes = type.implementedInterfaces.flatMap((interfaceName) => {
491
+ const routeMap = interfaceRoutes.get(interfaceName);
492
+ if (!routeMap)
493
+ return [];
494
+ const routes = routeMap.get(method.name) ?? [];
495
+ return routes.map((route) => ({
496
+ method: route.method,
497
+ path: joinPath(type.classPrefix, route.path),
498
+ }));
499
+ });
500
+ for (const route of inheritedRoutes) {
501
+ const detections = detectionsByFile.get(type.filePath) ?? [];
502
+ detections.push({
503
+ role: 'provider',
504
+ framework: 'spring',
505
+ method: route.method,
506
+ path: route.path,
507
+ name: method.name,
508
+ confidence: 0.8,
509
+ });
510
+ detectionsByFile.set(type.filePath, detections);
511
+ }
512
+ }
513
+ }
514
+ return [...detectionsByFile.entries()].map(([filePath, detections]) => ({
515
+ filePath,
516
+ detections,
517
+ }));
518
+ }
344
519
  export const JAVA_HTTP_PLUGIN = {
345
520
  name: 'java-http',
346
521
  language: Java,
347
522
  scan(tree) {
348
523
  const out = [];
349
524
  // ─── Providers: Spring class prefix + method annotations ────────
350
- const prefixByClassId = new Map();
351
- for (const match of runCompiledPatterns(SPRING_CLASS_PREFIX_PATTERNS, tree)) {
352
- const prefixNode = match.captures.prefix;
353
- const classNode = match.captures.class;
354
- if (!prefixNode || !classNode)
355
- continue;
356
- const prefix = unquoteLiteral(prefixNode.text);
357
- if (prefix !== null)
358
- prefixByClassId.set(classNode.id, prefix);
359
- }
525
+ const prefixByTypeId = collectTypePrefixes(tree);
360
526
  const feignPrefixByInterfaceId = new Map();
361
527
  for (const match of runCompiledPatterns(FEIGN_INTERFACE_PREFIX_PATTERNS, tree)) {
362
528
  const prefixNode = match.captures.prefix;
@@ -395,7 +561,9 @@ export const JAVA_HTTP_PLUGIN = {
395
561
  continue;
396
562
  }
397
563
  const enclosingClass = findEnclosingClass(methodNode);
398
- const prefix = enclosingClass ? (prefixByClassId.get(enclosingClass.id) ?? '') : '';
564
+ if (!enclosingClass)
565
+ continue;
566
+ const prefix = prefixByTypeId.get(enclosingClass.id) ?? '';
399
567
  const fullPath = joinPath(prefix, rawPath);
400
568
  out.push({
401
569
  role: 'provider',
@@ -528,4 +696,5 @@ export const JAVA_HTTP_PLUGIN = {
528
696
  }
529
697
  return out;
530
698
  },
699
+ scanProject: scanSpringProject,
531
700
  };
@@ -36,6 +36,14 @@ export interface HttpDetection {
36
36
  /** Confidence in (0, 1]. Source-scan plugins typically use 0.7–0.8. */
37
37
  confidence: number;
38
38
  }
39
+ export interface HttpScanInput {
40
+ filePath: string;
41
+ tree: Parser.Tree;
42
+ }
43
+ export interface HttpFileDetections {
44
+ filePath: string;
45
+ detections: HttpDetection[];
46
+ }
39
47
  /**
40
48
  * One language-scoped HTTP plugin. The plugin owns the tree-sitter
41
49
  * grammar and the `scan` function that translates a parsed tree into
@@ -90,4 +98,10 @@ export interface HttpLanguagePlugin {
90
98
  * single-file plugins can keep their unary `scan(tree)` shape.
91
99
  */
92
100
  scan(tree: Parser.Tree, repoContext?: RepoContext, fileRel?: string): HttpDetection[];
101
+ /**
102
+ * Optional project-level scan hook for language rules that require
103
+ * multiple files, such as Java controllers inheriting Spring mappings
104
+ * from annotated interfaces.
105
+ */
106
+ scanProject?(files: readonly HttpScanInput[]): HttpFileDetections[];
93
107
  }
@@ -4,7 +4,7 @@ import Parser from 'tree-sitter';
4
4
  import { createIgnoreFilter } from '../../../config/ignore-service.js';
5
5
  import { readSafe } from './fs-utils.js';
6
6
  import { parseSourceSafe } from '../../tree-sitter/safe-parse.js';
7
- import { getPluginForFile, HTTP_SCAN_GLOB } from './http-patterns/index.js';
7
+ import { getPluginForFile, HTTP_SCAN_GLOB, } from './http-patterns/index.js';
8
8
  /**
9
9
  * Language-agnostic orchestrator for HTTP route (provider + consumer)
10
10
  * contract extraction. Two strategies, in order of preference per role:
@@ -141,6 +141,9 @@ export class HttpRouteExtractor {
141
141
  // both graph-assisted enrichment and source-scan emission.
142
142
  const parser = new Parser();
143
143
  const cachedDetections = new Map();
144
+ const cachedInputs = new Map();
145
+ const projectDetections = new Map();
146
+ let projectScanComplete = false;
144
147
  // Per-plugin cross-file context (e.g. Python's FastAPI router →
145
148
  // include_router(prefix=...) map). Built lazily on first
146
149
  // `getDetections` call for a file the plugin handles, scoped to the
@@ -169,33 +172,45 @@ export class HttpRouteExtractor {
169
172
  return undefined;
170
173
  }
171
174
  };
172
- const getDetections = async (rel) => {
173
- const cached = cachedDetections.get(rel);
174
- if (cached)
175
- return cached;
175
+ const getScanInput = async (rel) => {
176
+ if (cachedInputs.has(rel))
177
+ return cachedInputs.get(rel) ?? null;
176
178
  const plugin = getPluginForFile(rel);
177
179
  if (!plugin) {
178
- cachedDetections.set(rel, []);
179
- return [];
180
+ cachedInputs.set(rel, null);
181
+ return null;
180
182
  }
181
183
  const repoContext = await ensureRepoContext(plugin);
182
184
  const content = readSafe(repoPath, rel);
183
185
  if (!content) {
184
- cachedDetections.set(rel, []);
185
- return [];
186
+ cachedInputs.set(rel, null);
187
+ return null;
186
188
  }
187
189
  try {
188
190
  parser.setLanguage(plugin.language);
189
191
  const tree = parseSourceSafe(parser, content);
190
- const detections = plugin.scan(tree, repoContext, rel);
191
- cachedDetections.set(rel, detections);
192
- return detections;
192
+ const input = { filePath: rel, tree };
193
+ const item = { plugin, input, repoContext };
194
+ cachedInputs.set(rel, item);
195
+ return item;
193
196
  }
194
197
  catch {
195
- cachedDetections.set(rel, []);
196
- return [];
198
+ cachedInputs.set(rel, null);
199
+ return null;
197
200
  }
198
201
  };
202
+ const getDetections = async (rel) => {
203
+ const cached = cachedDetections.get(rel);
204
+ if (cached)
205
+ return cached;
206
+ const scanInput = await getScanInput(rel);
207
+ const ownDetections = scanInput
208
+ ? scanInput.plugin.scan(scanInput.input.tree, scanInput.repoContext, rel)
209
+ : [];
210
+ const detections = [...ownDetections, ...(projectDetections.get(rel) ?? [])];
211
+ cachedDetections.set(rel, detections);
212
+ return detections;
213
+ };
199
214
  // Glob the source-scan file list at most once per extract() —
200
215
  // both provider and consumer fallback paths share the same list.
201
216
  let scannedFiles = null;
@@ -205,12 +220,36 @@ export class HttpRouteExtractor {
205
220
  scannedFiles = await this.scanFiles(repoPath);
206
221
  return scannedFiles;
207
222
  };
223
+ const collectProjectDetections = async (files) => {
224
+ if (projectScanComplete)
225
+ return;
226
+ projectScanComplete = true;
227
+ const byPlugin = new Map();
228
+ for (const rel of files) {
229
+ const scanInput = await getScanInput(rel);
230
+ if (!scanInput?.plugin.scanProject)
231
+ continue;
232
+ const items = byPlugin.get(scanInput.plugin) ?? [];
233
+ items.push(scanInput.input);
234
+ byPlugin.set(scanInput.plugin, items);
235
+ }
236
+ for (const [plugin, inputs] of byPlugin) {
237
+ const results = plugin.scanProject?.(inputs) ?? [];
238
+ for (const result of results) {
239
+ const existing = projectDetections.get(result.filePath) ?? [];
240
+ projectDetections.set(result.filePath, [...existing, ...result.detections]);
241
+ }
242
+ }
243
+ cachedDetections.clear();
244
+ };
245
+ const files = await getScannedFiles();
246
+ await collectProjectDetections(files);
208
247
  const graphProviders = dbExecutor != null ? await this.extractProvidersGraph(dbExecutor, getDetections) : [];
209
248
  // Source scan always runs to capture routes in languages/files not covered
210
249
  // by graph edges; the glob and per-file parse results are cached above.
211
- const providers = this.mergeGraphAndSourceContracts(graphProviders, await this.extractProvidersSourceScan(await getScannedFiles(), getDetections));
250
+ const providers = this.mergeGraphAndSourceContracts(graphProviders, await this.extractProvidersSourceScan(files, getDetections));
212
251
  const graphConsumers = dbExecutor != null ? await this.extractConsumersGraph(dbExecutor, getDetections) : [];
213
- const consumers = this.mergeGraphAndSourceContracts(graphConsumers, await this.extractConsumersSourceScan(await getScannedFiles(), getDetections));
252
+ const consumers = this.mergeGraphAndSourceContracts(graphConsumers, await this.extractConsumersSourceScan(files, getDetections));
214
253
  return [...providers, ...consumers];
215
254
  }
216
255
  async scanFiles(repoPath) {
@@ -28,6 +28,55 @@ import { goCallConfig } from '../call-extractors/configs/go.js';
28
28
  import { createHeritageExtractor } from '../heritage-extractors/generic.js';
29
29
  import { goHeritageConfig } from '../heritage-extractors/configs/go.js';
30
30
  import { emitGoScopeCaptures, goArityCompatibility, goBindingScopeFor, goImportOwningScope, goReceiverBinding, interpretGoImport, interpretGoTypeBinding, } from './go/index.js';
31
+ const GO_BUILT_INS = new Set([
32
+ // built-in functions
33
+ 'make',
34
+ 'new',
35
+ 'len',
36
+ 'cap',
37
+ 'append',
38
+ 'copy',
39
+ 'delete',
40
+ 'close',
41
+ 'panic',
42
+ 'recover',
43
+ 'print',
44
+ 'println',
45
+ 'complex',
46
+ 'real',
47
+ 'imag',
48
+ 'clear',
49
+ 'min',
50
+ 'max',
51
+ // built-in types
52
+ 'error',
53
+ 'bool',
54
+ 'string',
55
+ 'int',
56
+ 'int8',
57
+ 'int16',
58
+ 'int32',
59
+ 'int64',
60
+ 'uint',
61
+ 'uint8',
62
+ 'uint16',
63
+ 'uint32',
64
+ 'uint64',
65
+ 'uintptr',
66
+ 'float32',
67
+ 'float64',
68
+ 'complex64',
69
+ 'complex128',
70
+ 'byte',
71
+ 'rune',
72
+ 'any',
73
+ 'comparable',
74
+ // built-in values
75
+ 'true',
76
+ 'false',
77
+ 'nil',
78
+ 'iota',
79
+ ]);
31
80
  export const goProvider = defineLanguage({
32
81
  id: SupportedLanguages.Go,
33
82
  extensions: ['.go'],
@@ -81,6 +130,7 @@ export const goProvider = defineLanguage({
81
130
  variableExtractor: createVariableExtractor(goVariableConfig),
82
131
  classExtractor: createClassExtractor(goClassConfig),
83
132
  heritageExtractor: createHeritageExtractor(goHeritageConfig),
133
+ builtInNames: GO_BUILT_INS,
84
134
  // ── RFC #909 Ring 3: scope-based resolution hooks ──────────
85
135
  emitScopeCaptures: emitGoScopeCaptures,
86
136
  interpretImport: interpretGoImport,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.6-rc.83",
3
+ "version": "1.6.6-rc.85",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",