prinfer 0.5.3 → 0.6.0

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
@@ -6,7 +6,8 @@
6
6
 
7
7
  **Typehints for your AI agent.**
8
8
 
9
- prinfer gives AI coding assistants the ability to inspect TypeScript's inferred types so they can write cleaner code without redundant type annotations.
9
+ Give AI coding assistant the ability to inspect TypeScript's inferred types mimicking the IDE's hover behavior.
10
+ so they can write cleaner code without redundant type annotations.
10
11
 
11
12
  ## Why?
12
13
 
@@ -40,13 +41,15 @@ prinfer setup
40
41
 
41
42
  ### MCP Server (`prinfer-mcp`)
42
43
 
43
- Your agent gets an `infer_type` tool to check what TypeScript infers:
44
+ Your agent gets a `hover` tool to check what TypeScript infers at any position:
44
45
 
45
46
  ```
46
- infer_type(file: "src/utils.ts", name: "myFunction")
47
- infer_type(file: "src/utils.ts", name: "commandResult", line: 75)
47
+ hover(file: "src/utils.ts", line: 75, column: 10)
48
+ hover(file: "src/utils.ts", line: 75, column: 10, include_docs: true)
48
49
  ```
49
50
 
51
+ The position-based API matches IDE behavior and returns instantiated generic types at call sites.
52
+
50
53
  ### Claude Skill (`~/.claude/skills/prefer-infer.md`)
51
54
 
52
55
  A coding guideline that encourages your agent to:
@@ -55,7 +58,7 @@ A coding guideline that encourages your agent to:
55
58
  - Use prinfer to verify types before adding redundant hints
56
59
  - Write idiomatic TypeScript
57
60
 
58
- Plus a `/check-type` command for quick lookups.
61
+ Plus a `/hover` command for quick lookups.
59
62
 
60
63
  ## Manual Setup
61
64
 
@@ -74,8 +77,9 @@ claude mcp add prinfer node /path/to/node_modules/prinfer/dist/mcp.js
74
77
  prinfer also works as a standalone CLI:
75
78
 
76
79
  ```bash
77
- prinfer src/utils.ts myFunction
78
- prinfer src/utils.ts:75 commandResult
80
+ prinfer src/utils.ts:75:10
81
+ prinfer src/utils.ts:75:10 --docs
82
+ prinfer src/utils.ts:75:10 --project ./tsconfig.json
79
83
  ```
80
84
 
81
85
  Output:
@@ -83,18 +87,25 @@ Output:
83
87
  ```
84
88
  (x: number, y: string) => boolean
85
89
  returns: boolean
90
+ name: myFunction
91
+ kind: function
92
+ docs: Adds two numbers together.
86
93
  ```
87
94
 
88
95
  ## Programmatic API
89
96
 
90
97
  ```typescript
91
- import { inferType } from "prinfer";
98
+ import { hover } from "prinfer";
99
+
100
+ const result = hover("./src/utils.ts", 75, 10);
101
+ // => { signature: "(x: number, y: string) => boolean", returnType: "boolean", line: 75, column: 10, kind: "function", name: "myFunction" }
92
102
 
93
- const result = inferType("./src/utils.ts", "myFunction");
94
- // => { signature: "(x: number, y: string) => boolean", returnType: "boolean", line: 4 }
103
+ // With documentation
104
+ const result2 = hover("./src/utils.ts", 75, 10, { include_docs: true });
105
+ // => { ..., documentation: "Adds two numbers together." }
95
106
 
96
- // With line number for disambiguation
97
- const result2 = inferType("./src/utils.ts", "commandResult", { line: 75 });
107
+ // With custom tsconfig
108
+ const result3 = hover("./src/utils.ts", 75, 10, { project: "./tsconfig.json" });
98
109
  ```
99
110
 
100
111
  ## Requirements
package/dist/cli.cjs CHANGED
@@ -72,66 +72,203 @@ function loadProgram(entryFileAbs, project) {
72
72
  function isArrowOrFnExpr(n) {
73
73
  return !!n && (import_typescript.default.isArrowFunction(n) || import_typescript.default.isFunctionExpression(n));
74
74
  }
75
- function nodeNameText(n) {
76
- if (!n) return void 0;
77
- if (import_typescript.default.isIdentifier(n)) return n.text;
78
- if (import_typescript.default.isStringLiteral(n)) return n.text;
79
- if (import_typescript.default.isNumericLiteral(n)) return n.text;
80
- return void 0;
81
- }
82
- function isFunctionLikeNamed(node, name) {
83
- if (import_typescript.default.isFunctionDeclaration(node) && node.name?.text === name) return true;
84
- if (import_typescript.default.isVariableDeclaration(node) && import_typescript.default.isIdentifier(node.name) && node.name.text === name) {
85
- return isArrowOrFnExpr(node.initializer);
86
- }
87
- if ((import_typescript.default.isMethodDeclaration(node) || import_typescript.default.isMethodSignature(node)) && nodeNameText(node.name) === name) {
88
- return true;
89
- }
90
- if (import_typescript.default.isPropertyAssignment(node) && nodeNameText(node.name) === name) {
91
- return isArrowOrFnExpr(node.initializer);
75
+ function findSmallestNodeAtPosition(sourceFile, position) {
76
+ let result;
77
+ function visit(node) {
78
+ const start = node.getStart(sourceFile);
79
+ const end = node.getEnd();
80
+ if (position < start || position >= end) {
81
+ return;
82
+ }
83
+ if (!result || node.getWidth(sourceFile) <= result.getWidth(sourceFile)) {
84
+ result = node;
85
+ }
86
+ import_typescript.default.forEachChild(node, visit);
92
87
  }
93
- return false;
88
+ visit(sourceFile);
89
+ return result;
94
90
  }
95
- function isVariableNamed(node, name) {
96
- if (import_typescript.default.isVariableDeclaration(node) && import_typescript.default.isIdentifier(node.name) && node.name.text === name) {
91
+ function findHoverableAncestor(sourceFile, position) {
92
+ const smallestNode = findSmallestNodeAtPosition(sourceFile, position);
93
+ if (!smallestNode) return void 0;
94
+ let bestMatch = smallestNode;
95
+ function visit(n) {
96
+ const start = n.getStart(sourceFile);
97
+ const end = n.getEnd();
98
+ if (position < start || position >= end) {
99
+ return false;
100
+ }
101
+ if (import_typescript.default.isCallExpression(n)) {
102
+ const expr = n.expression;
103
+ if (import_typescript.default.isIdentifier(expr)) {
104
+ if (position >= expr.getStart(sourceFile) && position < expr.getEnd()) {
105
+ bestMatch = n;
106
+ }
107
+ } else if (import_typescript.default.isPropertyAccessExpression(expr)) {
108
+ if (position >= expr.name.getStart(sourceFile) && position < expr.name.getEnd()) {
109
+ bestMatch = n;
110
+ }
111
+ }
112
+ }
113
+ if (import_typescript.default.isFunctionDeclaration(n) && n.name) {
114
+ if (position >= n.name.getStart(sourceFile) && position < n.name.getEnd()) {
115
+ bestMatch = n;
116
+ }
117
+ }
118
+ if (import_typescript.default.isVariableDeclaration(n) && import_typescript.default.isIdentifier(n.name)) {
119
+ if (position >= n.name.getStart(sourceFile) && position < n.name.getEnd()) {
120
+ bestMatch = n;
121
+ }
122
+ }
123
+ if ((import_typescript.default.isMethodDeclaration(n) || import_typescript.default.isMethodSignature(n)) && import_typescript.default.isIdentifier(n.name)) {
124
+ if (position >= n.name.getStart(sourceFile) && position < n.name.getEnd()) {
125
+ bestMatch = n;
126
+ }
127
+ }
128
+ if (import_typescript.default.isClassDeclaration(n) && n.name) {
129
+ if (position >= n.name.getStart(sourceFile) && position < n.name.getEnd()) {
130
+ bestMatch = n;
131
+ }
132
+ }
133
+ if (import_typescript.default.isInterfaceDeclaration(n) && n.name) {
134
+ if (position >= n.name.getStart(sourceFile) && position < n.name.getEnd()) {
135
+ bestMatch = n;
136
+ }
137
+ }
138
+ import_typescript.default.forEachChild(n, visit);
97
139
  return true;
98
140
  }
99
- return false;
100
- }
101
- function isNamedNode(node, name) {
102
- return isFunctionLikeNamed(node, name) || isVariableNamed(node, name);
141
+ visit(sourceFile);
142
+ return bestMatch;
103
143
  }
104
- function getLineNumber(sourceFile, node) {
105
- const { line } = sourceFile.getLineAndCharacterOfPosition(
106
- node.getStart(sourceFile)
144
+ function findNodeAtPosition(sourceFile, line, column) {
145
+ const lineCount = sourceFile.getLineStarts().length;
146
+ if (line < 1 || line > lineCount) {
147
+ return void 0;
148
+ }
149
+ const lineStart = sourceFile.getLineStarts()[line - 1];
150
+ const lineEnd = line < lineCount ? sourceFile.getLineStarts()[line] : sourceFile.getEnd();
151
+ const lineLength = lineEnd - lineStart;
152
+ if (column < 1 || column > lineLength + 1) {
153
+ return void 0;
154
+ }
155
+ const position = sourceFile.getPositionOfLineAndCharacter(
156
+ line - 1,
157
+ column - 1
107
158
  );
108
- return line + 1;
159
+ return findHoverableAncestor(sourceFile, position);
109
160
  }
110
- function findNodeByNameAndLine(sourceFile, name, line) {
111
- let found;
112
- const visit = (node) => {
113
- if (found) return;
114
- if (isNamedNode(node, name)) {
115
- if (line !== void 0) {
116
- const nodeLine = getLineNumber(sourceFile, node);
117
- if (nodeLine === line) {
118
- found = node;
119
- return;
120
- }
121
- } else {
122
- found = node;
123
- return;
124
- }
161
+ function getSymbolKind(node) {
162
+ if (import_typescript.default.isFunctionDeclaration(node)) return "function";
163
+ if (import_typescript.default.isArrowFunction(node)) return "function";
164
+ if (import_typescript.default.isFunctionExpression(node)) return "function";
165
+ if (import_typescript.default.isMethodDeclaration(node)) return "method";
166
+ if (import_typescript.default.isMethodSignature(node)) return "method";
167
+ if (import_typescript.default.isVariableDeclaration(node)) {
168
+ const init = node.initializer;
169
+ if (init && (import_typescript.default.isArrowFunction(init) || import_typescript.default.isFunctionExpression(init))) {
170
+ return "function";
125
171
  }
126
- import_typescript.default.forEachChild(node, visit);
127
- };
128
- visit(sourceFile);
129
- return found;
172
+ return "variable";
173
+ }
174
+ if (import_typescript.default.isParameter(node)) return "parameter";
175
+ if (import_typescript.default.isPropertyDeclaration(node)) return "property";
176
+ if (import_typescript.default.isPropertySignature(node)) return "property";
177
+ if (import_typescript.default.isPropertyAccessExpression(node)) return "property";
178
+ if (import_typescript.default.isCallExpression(node)) return "call";
179
+ if (import_typescript.default.isTypeAliasDeclaration(node)) return "type";
180
+ if (import_typescript.default.isInterfaceDeclaration(node)) return "interface";
181
+ if (import_typescript.default.isClassDeclaration(node)) return "class";
182
+ if (import_typescript.default.isIdentifier(node)) return "identifier";
183
+ return "unknown";
184
+ }
185
+ function getNodeName(node) {
186
+ if (import_typescript.default.isFunctionDeclaration(node) && node.name) {
187
+ return node.name.text;
188
+ }
189
+ if (import_typescript.default.isVariableDeclaration(node) && import_typescript.default.isIdentifier(node.name)) {
190
+ return node.name.text;
191
+ }
192
+ if ((import_typescript.default.isMethodDeclaration(node) || import_typescript.default.isMethodSignature(node)) && import_typescript.default.isIdentifier(node.name)) {
193
+ return node.name.text;
194
+ }
195
+ if (import_typescript.default.isPropertyAccessExpression(node)) {
196
+ return node.name.text;
197
+ }
198
+ if (import_typescript.default.isCallExpression(node)) {
199
+ const expr = node.expression;
200
+ if (import_typescript.default.isIdentifier(expr)) return expr.text;
201
+ if (import_typescript.default.isPropertyAccessExpression(expr)) return expr.name.text;
202
+ }
203
+ if (import_typescript.default.isParameter(node) && import_typescript.default.isIdentifier(node.name)) {
204
+ return node.name.text;
205
+ }
206
+ if (import_typescript.default.isIdentifier(node)) {
207
+ return node.text;
208
+ }
209
+ if (import_typescript.default.isTypeAliasDeclaration(node) || import_typescript.default.isInterfaceDeclaration(node) || import_typescript.default.isClassDeclaration(node)) {
210
+ return node.name?.text;
211
+ }
212
+ return void 0;
130
213
  }
131
- function getTypeInfo(program, node, sourceFile) {
214
+ function getDocumentation(checker, symbol) {
215
+ if (!symbol) return void 0;
216
+ const docs = symbol.getDocumentationComment(checker);
217
+ if (docs.length === 0) return void 0;
218
+ return import_typescript.default.displayPartsToString(docs);
219
+ }
220
+ function getHoverInfo(program, node, sourceFile, includeDocs) {
132
221
  const checker = program.getTypeChecker();
133
- const sf = sourceFile ?? node.getSourceFile();
134
- const line = getLineNumber(sf, node);
222
+ const sf = sourceFile;
223
+ const { line, character } = sf.getLineAndCharacterOfPosition(
224
+ node.getStart(sf)
225
+ );
226
+ const flags = import_typescript.default.TypeFormatFlags.NoTruncation;
227
+ const kind = getSymbolKind(node);
228
+ const name = getNodeName(node);
229
+ let symbol;
230
+ if (import_typescript.default.isCallExpression(node)) {
231
+ const expr = node.expression;
232
+ if (import_typescript.default.isPropertyAccessExpression(expr)) {
233
+ symbol = checker.getSymbolAtLocation(expr.name);
234
+ } else {
235
+ symbol = checker.getSymbolAtLocation(expr);
236
+ }
237
+ } else {
238
+ const nodeWithName2 = node;
239
+ if (nodeWithName2.name) {
240
+ symbol = checker.getSymbolAtLocation(nodeWithName2.name);
241
+ } else {
242
+ symbol = checker.getSymbolAtLocation(node);
243
+ }
244
+ }
245
+ const documentation = includeDocs ? getDocumentation(checker, symbol) : void 0;
246
+ if (import_typescript.default.isCallExpression(node)) {
247
+ const sig2 = checker.getResolvedSignature(node);
248
+ if (sig2) {
249
+ const signature = checker.signatureToString(sig2, void 0, flags);
250
+ const ret = checker.getReturnTypeOfSignature(sig2);
251
+ const returnType = checker.typeToString(ret, void 0, flags);
252
+ return {
253
+ signature,
254
+ returnType,
255
+ line: line + 1,
256
+ column: character + 1,
257
+ documentation,
258
+ kind,
259
+ name
260
+ };
261
+ }
262
+ const t2 = checker.getTypeAtLocation(node);
263
+ return {
264
+ signature: checker.typeToString(t2, void 0, flags),
265
+ line: line + 1,
266
+ column: character + 1,
267
+ documentation,
268
+ kind,
269
+ name
270
+ };
271
+ }
135
272
  let sig;
136
273
  if (import_typescript.default.isFunctionDeclaration(node) || import_typescript.default.isMethodDeclaration(node)) {
137
274
  sig = checker.getSignatureFromDeclaration(node) ?? void 0;
@@ -146,26 +283,39 @@ function getTypeInfo(program, node, sourceFile) {
146
283
  } else if (import_typescript.default.isMethodSignature(node)) {
147
284
  sig = checker.getSignatureFromDeclaration(node) ?? void 0;
148
285
  }
149
- const flags = import_typescript.default.TypeFormatFlags.NoTruncation;
150
286
  if (sig) {
151
287
  const signature = checker.signatureToString(sig, void 0, flags);
152
288
  const ret = checker.getReturnTypeOfSignature(sig);
153
289
  const returnType = checker.typeToString(ret, void 0, flags);
154
- return { signature, returnType, line };
290
+ return {
291
+ signature,
292
+ returnType,
293
+ line: line + 1,
294
+ column: character + 1,
295
+ documentation,
296
+ kind,
297
+ name
298
+ };
155
299
  }
156
300
  const nodeWithName = node;
157
- let nameNode = node;
301
+ let targetNode = node;
158
302
  if (nodeWithName.name && import_typescript.default.isIdentifier(nodeWithName.name)) {
159
- nameNode = nodeWithName.name;
303
+ targetNode = nodeWithName.name;
160
304
  }
161
- const t = checker.getTypeAtLocation(nameNode);
162
- return { signature: checker.typeToString(t, void 0, flags), line };
305
+ const t = checker.getTypeAtLocation(targetNode);
306
+ return {
307
+ signature: checker.typeToString(t, void 0, flags),
308
+ line: line + 1,
309
+ column: character + 1,
310
+ documentation,
311
+ kind,
312
+ name
313
+ };
163
314
  }
164
315
 
165
316
  // src/index.ts
166
- function inferType(file, name, options) {
167
- const opts = typeof options === "string" ? { project: options } : options ?? {};
168
- const { line, project } = opts;
317
+ function hover(file, line, column, options) {
318
+ const { project, include_docs = false } = options ?? {};
169
319
  const entryFileAbs = import_node_path2.default.resolve(process.cwd(), file);
170
320
  if (!import_node_fs.default.existsSync(entryFileAbs)) {
171
321
  throw new Error(`File not found: ${entryFileAbs}`);
@@ -177,14 +327,11 @@ function inferType(file, name, options) {
177
327
  `Could not load source file into the program (check tsconfig include/exclude): ${entryFileAbs}`
178
328
  );
179
329
  }
180
- const node = findNodeByNameAndLine(sourceFile, name, line);
330
+ const node = findNodeAtPosition(sourceFile, line, column);
181
331
  if (!node) {
182
- const lineInfo = line !== void 0 ? ` at line ${line}` : "";
183
- throw new Error(
184
- `No symbol named "${name}"${lineInfo} found in ${entryFileAbs}`
185
- );
332
+ throw new Error(`No symbol found at ${entryFileAbs}:${line}:${column}`);
186
333
  }
187
- return getTypeInfo(program, node, sourceFile);
334
+ return getHoverInfo(program, node, sourceFile, include_docs);
188
335
  }
189
336
 
190
337
  // src/cli.ts
@@ -193,24 +340,24 @@ var HELP = `
193
340
  prinfer - TypeScript type inference inspection tool
194
341
 
195
342
  Usage:
196
- prinfer <file.ts>[:<line>] <name> [--project <tsconfig.json>]
343
+ prinfer <file.ts>:<line>:<column> [--docs] [--project <tsconfig.json>]
197
344
  prinfer setup
198
345
 
199
346
  Commands:
200
347
  setup Install MCP server and skill for Claude Code
201
348
 
202
349
  Arguments:
203
- file.ts Path to the TypeScript file
204
- :line Optional line number to narrow search (e.g., file.ts:75)
205
- name Name of the function/variable to inspect
350
+ file.ts:line:column Path to TypeScript file with 1-based line and column
206
351
 
207
352
  Options:
353
+ --docs, -d Include JSDoc/TSDoc documentation
208
354
  --project, -p Path to tsconfig.json (optional)
209
355
  --help, -h Show this help message
210
356
 
211
357
  Examples:
212
- prinfer src/utils.ts myFunction
213
- prinfer src/utils.ts:75 commandResult
358
+ prinfer src/utils.ts:75:10
359
+ prinfer src/utils.ts:75:10 --docs
360
+ prinfer src/utils.ts:75:10 --project ./tsconfig.json
214
361
  prinfer setup
215
362
  `.trim();
216
363
  var MANUAL_SETUP = `
@@ -256,26 +403,26 @@ When writing TypeScript code, prefer relying on type inference over explicit typ
256
403
  - The type serves as documentation for complex structures
257
404
  - You're defining a public API contract
258
405
 
259
- Use the \`prinfer\` MCP tool (\`infer_type\`) to verify what TypeScript infers before adding explicit types.
406
+ Use the \`prinfer\` MCP tool (\`hover\`) to verify what TypeScript infers before adding explicit types.
260
407
 
261
408
  ## Commands
262
409
 
263
- ### /check-type
410
+ ### /hover
264
411
 
265
- Check the inferred type of a TypeScript symbol.
412
+ Check the inferred type at a specific position in a TypeScript file.
266
413
 
267
- Usage: \`/check-type <file>:<line> <name>\` or \`/check-type <file> <name>\`
414
+ Usage: \`/hover <file>:<line>:<column>\`
268
415
 
269
416
  Examples:
270
- - \`/check-type src/utils.ts:75 commandResult\`
271
- - \`/check-type src/utils.ts myFunction\`
417
+ - \`/hover src/utils.ts:75:10\`
418
+ - \`/hover src/utils.ts:42:5\`
272
419
 
273
- <command-name>check-type</command-name>
420
+ <command-name>hover</command-name>
274
421
 
275
- Use the \`infer_type\` MCP tool to check the type:
276
- 1. Parse the arguments to extract file, optional line number, and symbol name
277
- 2. Call \`infer_type(file, name, line?)\`
278
- 3. Report the inferred signature and return type
422
+ Use the \`hover\` MCP tool to check the type:
423
+ 1. Parse the arguments to extract file, line, and column
424
+ 2. Call \`hover(file, line, column, { include_docs: true })\`
425
+ 3. Report the inferred signature, return type, and documentation
279
426
  `;
280
427
  function runSetup() {
281
428
  const homeDir = import_node_os.default.homedir();
@@ -317,12 +464,16 @@ function runSetup() {
317
464
  process.exit(1);
318
465
  }
319
466
  }
320
- function parseFileArg(arg) {
321
- const match = arg.match(/^(.+):(\d+)$/);
467
+ function parsePositionArg(arg) {
468
+ const match = arg.match(/^(.+):(\d+):(\d+)$/);
322
469
  if (match) {
323
- return { file: match[1], line: Number.parseInt(match[2], 10) };
470
+ return {
471
+ file: match[1],
472
+ line: Number.parseInt(match[2], 10),
473
+ column: Number.parseInt(match[3], 10)
474
+ };
324
475
  }
325
- return { file: arg };
476
+ return null;
326
477
  }
327
478
  function parseArgs(argv) {
328
479
  const args = argv.slice(2);
@@ -334,16 +485,17 @@ function parseArgs(argv) {
334
485
  runSetup();
335
486
  return null;
336
487
  }
337
- const fileArg = args[0];
338
- const name = args[1];
339
- if (!fileArg || !name) {
488
+ const positionArg = args[0];
489
+ const parsed = parsePositionArg(positionArg);
490
+ if (!parsed) {
340
491
  console.error(
341
- "Error: Both <file> and <name> arguments are required.\n"
492
+ "Error: Position argument must be in format <file>:<line>:<column>\n"
342
493
  );
343
494
  console.log(HELP);
344
495
  process.exit(1);
345
496
  }
346
- const { file, line } = parseFileArg(fileArg);
497
+ const { file, line, column } = parsed;
498
+ const includeDocs = args.includes("--docs") || args.includes("-d");
347
499
  let project;
348
500
  const projectIdx = args.findIndex((a) => a === "--project" || a === "-p");
349
501
  if (projectIdx >= 0) {
@@ -354,7 +506,7 @@ function parseArgs(argv) {
354
506
  process.exit(1);
355
507
  }
356
508
  }
357
- return { file, name, line, project };
509
+ return { file, line, column, includeDocs, project };
358
510
  }
359
511
  function main() {
360
512
  const options = parseArgs(process.argv);
@@ -362,14 +514,21 @@ function main() {
362
514
  process.exit(0);
363
515
  }
364
516
  try {
365
- const result = inferType(options.file, options.name, {
366
- line: options.line,
517
+ const result = hover(options.file, options.line, options.column, {
518
+ include_docs: options.includeDocs,
367
519
  project: options.project
368
520
  });
369
521
  console.log(result.signature);
370
522
  if (result.returnType) {
371
523
  console.log("returns:", result.returnType);
372
524
  }
525
+ if (result.name) {
526
+ console.log("name:", result.name);
527
+ }
528
+ console.log("kind:", result.kind);
529
+ if (result.documentation) {
530
+ console.log("docs:", result.documentation);
531
+ }
373
532
  } catch (error) {
374
533
  console.error(error.message);
375
534
  process.exit(1);