prinfer 0.5.2 → 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/dist/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
+ import { execSync } from "child_process";
4
5
  import fs2 from "fs";
5
6
  import os from "os";
6
7
  import path3 from "path";
@@ -48,66 +49,203 @@ function loadProgram(entryFileAbs, project) {
48
49
  function isArrowOrFnExpr(n) {
49
50
  return !!n && (ts.isArrowFunction(n) || ts.isFunctionExpression(n));
50
51
  }
51
- function nodeNameText(n) {
52
- if (!n) return void 0;
53
- if (ts.isIdentifier(n)) return n.text;
54
- if (ts.isStringLiteral(n)) return n.text;
55
- if (ts.isNumericLiteral(n)) return n.text;
56
- return void 0;
57
- }
58
- function isFunctionLikeNamed(node, name) {
59
- if (ts.isFunctionDeclaration(node) && node.name?.text === name) return true;
60
- if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === name) {
61
- return isArrowOrFnExpr(node.initializer);
62
- }
63
- if ((ts.isMethodDeclaration(node) || ts.isMethodSignature(node)) && nodeNameText(node.name) === name) {
64
- return true;
65
- }
66
- if (ts.isPropertyAssignment(node) && nodeNameText(node.name) === name) {
67
- return isArrowOrFnExpr(node.initializer);
52
+ function findSmallestNodeAtPosition(sourceFile, position) {
53
+ let result;
54
+ function visit(node) {
55
+ const start = node.getStart(sourceFile);
56
+ const end = node.getEnd();
57
+ if (position < start || position >= end) {
58
+ return;
59
+ }
60
+ if (!result || node.getWidth(sourceFile) <= result.getWidth(sourceFile)) {
61
+ result = node;
62
+ }
63
+ ts.forEachChild(node, visit);
68
64
  }
69
- return false;
65
+ visit(sourceFile);
66
+ return result;
70
67
  }
71
- function isVariableNamed(node, name) {
72
- if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === name) {
68
+ function findHoverableAncestor(sourceFile, position) {
69
+ const smallestNode = findSmallestNodeAtPosition(sourceFile, position);
70
+ if (!smallestNode) return void 0;
71
+ let bestMatch = smallestNode;
72
+ function visit(n) {
73
+ const start = n.getStart(sourceFile);
74
+ const end = n.getEnd();
75
+ if (position < start || position >= end) {
76
+ return false;
77
+ }
78
+ if (ts.isCallExpression(n)) {
79
+ const expr = n.expression;
80
+ if (ts.isIdentifier(expr)) {
81
+ if (position >= expr.getStart(sourceFile) && position < expr.getEnd()) {
82
+ bestMatch = n;
83
+ }
84
+ } else if (ts.isPropertyAccessExpression(expr)) {
85
+ if (position >= expr.name.getStart(sourceFile) && position < expr.name.getEnd()) {
86
+ bestMatch = n;
87
+ }
88
+ }
89
+ }
90
+ if (ts.isFunctionDeclaration(n) && n.name) {
91
+ if (position >= n.name.getStart(sourceFile) && position < n.name.getEnd()) {
92
+ bestMatch = n;
93
+ }
94
+ }
95
+ if (ts.isVariableDeclaration(n) && ts.isIdentifier(n.name)) {
96
+ if (position >= n.name.getStart(sourceFile) && position < n.name.getEnd()) {
97
+ bestMatch = n;
98
+ }
99
+ }
100
+ if ((ts.isMethodDeclaration(n) || ts.isMethodSignature(n)) && ts.isIdentifier(n.name)) {
101
+ if (position >= n.name.getStart(sourceFile) && position < n.name.getEnd()) {
102
+ bestMatch = n;
103
+ }
104
+ }
105
+ if (ts.isClassDeclaration(n) && n.name) {
106
+ if (position >= n.name.getStart(sourceFile) && position < n.name.getEnd()) {
107
+ bestMatch = n;
108
+ }
109
+ }
110
+ if (ts.isInterfaceDeclaration(n) && n.name) {
111
+ if (position >= n.name.getStart(sourceFile) && position < n.name.getEnd()) {
112
+ bestMatch = n;
113
+ }
114
+ }
115
+ ts.forEachChild(n, visit);
73
116
  return true;
74
117
  }
75
- return false;
76
- }
77
- function isNamedNode(node, name) {
78
- return isFunctionLikeNamed(node, name) || isVariableNamed(node, name);
118
+ visit(sourceFile);
119
+ return bestMatch;
79
120
  }
80
- function getLineNumber(sourceFile, node) {
81
- const { line } = sourceFile.getLineAndCharacterOfPosition(
82
- node.getStart(sourceFile)
121
+ function findNodeAtPosition(sourceFile, line, column) {
122
+ const lineCount = sourceFile.getLineStarts().length;
123
+ if (line < 1 || line > lineCount) {
124
+ return void 0;
125
+ }
126
+ const lineStart = sourceFile.getLineStarts()[line - 1];
127
+ const lineEnd = line < lineCount ? sourceFile.getLineStarts()[line] : sourceFile.getEnd();
128
+ const lineLength = lineEnd - lineStart;
129
+ if (column < 1 || column > lineLength + 1) {
130
+ return void 0;
131
+ }
132
+ const position = sourceFile.getPositionOfLineAndCharacter(
133
+ line - 1,
134
+ column - 1
83
135
  );
84
- return line + 1;
136
+ return findHoverableAncestor(sourceFile, position);
85
137
  }
86
- function findNodeByNameAndLine(sourceFile, name, line) {
87
- let found;
88
- const visit = (node) => {
89
- if (found) return;
90
- if (isNamedNode(node, name)) {
91
- if (line !== void 0) {
92
- const nodeLine = getLineNumber(sourceFile, node);
93
- if (nodeLine === line) {
94
- found = node;
95
- return;
96
- }
97
- } else {
98
- found = node;
99
- return;
100
- }
138
+ function getSymbolKind(node) {
139
+ if (ts.isFunctionDeclaration(node)) return "function";
140
+ if (ts.isArrowFunction(node)) return "function";
141
+ if (ts.isFunctionExpression(node)) return "function";
142
+ if (ts.isMethodDeclaration(node)) return "method";
143
+ if (ts.isMethodSignature(node)) return "method";
144
+ if (ts.isVariableDeclaration(node)) {
145
+ const init = node.initializer;
146
+ if (init && (ts.isArrowFunction(init) || ts.isFunctionExpression(init))) {
147
+ return "function";
101
148
  }
102
- ts.forEachChild(node, visit);
103
- };
104
- visit(sourceFile);
105
- return found;
149
+ return "variable";
150
+ }
151
+ if (ts.isParameter(node)) return "parameter";
152
+ if (ts.isPropertyDeclaration(node)) return "property";
153
+ if (ts.isPropertySignature(node)) return "property";
154
+ if (ts.isPropertyAccessExpression(node)) return "property";
155
+ if (ts.isCallExpression(node)) return "call";
156
+ if (ts.isTypeAliasDeclaration(node)) return "type";
157
+ if (ts.isInterfaceDeclaration(node)) return "interface";
158
+ if (ts.isClassDeclaration(node)) return "class";
159
+ if (ts.isIdentifier(node)) return "identifier";
160
+ return "unknown";
106
161
  }
107
- function getTypeInfo(program, node, sourceFile) {
162
+ function getNodeName(node) {
163
+ if (ts.isFunctionDeclaration(node) && node.name) {
164
+ return node.name.text;
165
+ }
166
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) {
167
+ return node.name.text;
168
+ }
169
+ if ((ts.isMethodDeclaration(node) || ts.isMethodSignature(node)) && ts.isIdentifier(node.name)) {
170
+ return node.name.text;
171
+ }
172
+ if (ts.isPropertyAccessExpression(node)) {
173
+ return node.name.text;
174
+ }
175
+ if (ts.isCallExpression(node)) {
176
+ const expr = node.expression;
177
+ if (ts.isIdentifier(expr)) return expr.text;
178
+ if (ts.isPropertyAccessExpression(expr)) return expr.name.text;
179
+ }
180
+ if (ts.isParameter(node) && ts.isIdentifier(node.name)) {
181
+ return node.name.text;
182
+ }
183
+ if (ts.isIdentifier(node)) {
184
+ return node.text;
185
+ }
186
+ if (ts.isTypeAliasDeclaration(node) || ts.isInterfaceDeclaration(node) || ts.isClassDeclaration(node)) {
187
+ return node.name?.text;
188
+ }
189
+ return void 0;
190
+ }
191
+ function getDocumentation(checker, symbol) {
192
+ if (!symbol) return void 0;
193
+ const docs = symbol.getDocumentationComment(checker);
194
+ if (docs.length === 0) return void 0;
195
+ return ts.displayPartsToString(docs);
196
+ }
197
+ function getHoverInfo(program, node, sourceFile, includeDocs) {
108
198
  const checker = program.getTypeChecker();
109
- const sf = sourceFile ?? node.getSourceFile();
110
- const line = getLineNumber(sf, node);
199
+ const sf = sourceFile;
200
+ const { line, character } = sf.getLineAndCharacterOfPosition(
201
+ node.getStart(sf)
202
+ );
203
+ const flags = ts.TypeFormatFlags.NoTruncation;
204
+ const kind = getSymbolKind(node);
205
+ const name = getNodeName(node);
206
+ let symbol;
207
+ if (ts.isCallExpression(node)) {
208
+ const expr = node.expression;
209
+ if (ts.isPropertyAccessExpression(expr)) {
210
+ symbol = checker.getSymbolAtLocation(expr.name);
211
+ } else {
212
+ symbol = checker.getSymbolAtLocation(expr);
213
+ }
214
+ } else {
215
+ const nodeWithName2 = node;
216
+ if (nodeWithName2.name) {
217
+ symbol = checker.getSymbolAtLocation(nodeWithName2.name);
218
+ } else {
219
+ symbol = checker.getSymbolAtLocation(node);
220
+ }
221
+ }
222
+ const documentation = includeDocs ? getDocumentation(checker, symbol) : void 0;
223
+ if (ts.isCallExpression(node)) {
224
+ const sig2 = checker.getResolvedSignature(node);
225
+ if (sig2) {
226
+ const signature = checker.signatureToString(sig2, void 0, flags);
227
+ const ret = checker.getReturnTypeOfSignature(sig2);
228
+ const returnType = checker.typeToString(ret, void 0, flags);
229
+ return {
230
+ signature,
231
+ returnType,
232
+ line: line + 1,
233
+ column: character + 1,
234
+ documentation,
235
+ kind,
236
+ name
237
+ };
238
+ }
239
+ const t2 = checker.getTypeAtLocation(node);
240
+ return {
241
+ signature: checker.typeToString(t2, void 0, flags),
242
+ line: line + 1,
243
+ column: character + 1,
244
+ documentation,
245
+ kind,
246
+ name
247
+ };
248
+ }
111
249
  let sig;
112
250
  if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) {
113
251
  sig = checker.getSignatureFromDeclaration(node) ?? void 0;
@@ -122,26 +260,39 @@ function getTypeInfo(program, node, sourceFile) {
122
260
  } else if (ts.isMethodSignature(node)) {
123
261
  sig = checker.getSignatureFromDeclaration(node) ?? void 0;
124
262
  }
125
- const flags = ts.TypeFormatFlags.NoTruncation;
126
263
  if (sig) {
127
264
  const signature = checker.signatureToString(sig, void 0, flags);
128
265
  const ret = checker.getReturnTypeOfSignature(sig);
129
266
  const returnType = checker.typeToString(ret, void 0, flags);
130
- return { signature, returnType, line };
267
+ return {
268
+ signature,
269
+ returnType,
270
+ line: line + 1,
271
+ column: character + 1,
272
+ documentation,
273
+ kind,
274
+ name
275
+ };
131
276
  }
132
277
  const nodeWithName = node;
133
- let nameNode = node;
278
+ let targetNode = node;
134
279
  if (nodeWithName.name && ts.isIdentifier(nodeWithName.name)) {
135
- nameNode = nodeWithName.name;
280
+ targetNode = nodeWithName.name;
136
281
  }
137
- const t = checker.getTypeAtLocation(nameNode);
138
- return { signature: checker.typeToString(t, void 0, flags), line };
282
+ const t = checker.getTypeAtLocation(targetNode);
283
+ return {
284
+ signature: checker.typeToString(t, void 0, flags),
285
+ line: line + 1,
286
+ column: character + 1,
287
+ documentation,
288
+ kind,
289
+ name
290
+ };
139
291
  }
140
292
 
141
293
  // src/index.ts
142
- function inferType(file, name, options) {
143
- const opts = typeof options === "string" ? { project: options } : options ?? {};
144
- const { line, project } = opts;
294
+ function hover(file, line, column, options) {
295
+ const { project, include_docs = false } = options ?? {};
145
296
  const entryFileAbs = path2.resolve(process.cwd(), file);
146
297
  if (!fs.existsSync(entryFileAbs)) {
147
298
  throw new Error(`File not found: ${entryFileAbs}`);
@@ -153,14 +304,11 @@ function inferType(file, name, options) {
153
304
  `Could not load source file into the program (check tsconfig include/exclude): ${entryFileAbs}`
154
305
  );
155
306
  }
156
- const node = findNodeByNameAndLine(sourceFile, name, line);
307
+ const node = findNodeAtPosition(sourceFile, line, column);
157
308
  if (!node) {
158
- const lineInfo = line !== void 0 ? ` at line ${line}` : "";
159
- throw new Error(
160
- `No symbol named "${name}"${lineInfo} found in ${entryFileAbs}`
161
- );
309
+ throw new Error(`No symbol found at ${entryFileAbs}:${line}:${column}`);
162
310
  }
163
- return getTypeInfo(program, node, sourceFile);
311
+ return getHoverInfo(program, node, sourceFile, include_docs);
164
312
  }
165
313
 
166
314
  // src/cli.ts
@@ -168,43 +316,56 @@ var HELP = `
168
316
  prinfer - TypeScript type inference inspection tool
169
317
 
170
318
  Usage:
171
- prinfer <file.ts>[:<line>] <name> [--project <tsconfig.json>]
319
+ prinfer <file.ts>:<line>:<column> [--docs] [--project <tsconfig.json>]
172
320
  prinfer setup
173
321
 
174
322
  Commands:
175
323
  setup Install MCP server and skill for Claude Code
176
324
 
177
325
  Arguments:
178
- file.ts Path to the TypeScript file
179
- :line Optional line number to narrow search (e.g., file.ts:75)
180
- name Name of the function/variable to inspect
326
+ file.ts:line:column Path to TypeScript file with 1-based line and column
181
327
 
182
328
  Options:
329
+ --docs, -d Include JSDoc/TSDoc documentation
183
330
  --project, -p Path to tsconfig.json (optional)
184
331
  --help, -h Show this help message
185
332
 
186
333
  Examples:
187
- prinfer src/utils.ts myFunction
188
- prinfer src/utils.ts:75 commandResult
334
+ prinfer src/utils.ts:75:10
335
+ prinfer src/utils.ts:75:10 --docs
336
+ prinfer src/utils.ts:75:10 --project ./tsconfig.json
189
337
  prinfer setup
190
338
  `.trim();
191
339
  var MANUAL_SETUP = `
192
340
  Manual setup instructions:
193
341
 
194
- 1. Add MCP server to ~/.claude/settings.json:
195
-
196
- {
197
- "mcpServers": {
198
- "prinfer": {
199
- "command": "prinfer-mcp"
200
- }
201
- }
202
- }
342
+ 1. Add MCP server:
343
+ Run: claude mcp add prinfer node /path/to/prinfer-mcp
203
344
 
204
345
  2. Create skill file at ~/.claude/skills/prefer-infer.md:
205
-
206
346
  See https://github.com/clockblocker/prinfer for skill content.
207
347
  `.trim();
348
+ function getMcpBinaryPath() {
349
+ const thisScript = new URL(import.meta.url).pathname;
350
+ return path3.join(path3.dirname(thisScript), "mcp.js");
351
+ }
352
+ function installMcpServer() {
353
+ try {
354
+ const mcpPath = getMcpBinaryPath();
355
+ try {
356
+ execSync("claude mcp remove prinfer", { stdio: "pipe" });
357
+ } catch {
358
+ }
359
+ execSync(`claude mcp add prinfer node ${mcpPath}`, {
360
+ stdio: "inherit"
361
+ });
362
+ console.log("[ok] Added prinfer MCP server");
363
+ return true;
364
+ } catch (err) {
365
+ console.error(`[error] MCP setup failed: ${err.message}`);
366
+ return false;
367
+ }
368
+ }
208
369
  var SKILL_CONTENT = `# TypeScript Type Inference (prinfer)
209
370
 
210
371
  ## Coding Guideline
@@ -218,26 +379,26 @@ When writing TypeScript code, prefer relying on type inference over explicit typ
218
379
  - The type serves as documentation for complex structures
219
380
  - You're defining a public API contract
220
381
 
221
- Use the \`prinfer\` MCP tool (\`infer_type\`) to verify what TypeScript infers before adding explicit types.
382
+ Use the \`prinfer\` MCP tool (\`hover\`) to verify what TypeScript infers before adding explicit types.
222
383
 
223
384
  ## Commands
224
385
 
225
- ### /check-type
386
+ ### /hover
226
387
 
227
- Check the inferred type of a TypeScript symbol.
388
+ Check the inferred type at a specific position in a TypeScript file.
228
389
 
229
- Usage: \`/check-type <file>:<line> <name>\` or \`/check-type <file> <name>\`
390
+ Usage: \`/hover <file>:<line>:<column>\`
230
391
 
231
392
  Examples:
232
- - \`/check-type src/utils.ts:75 commandResult\`
233
- - \`/check-type src/utils.ts myFunction\`
393
+ - \`/hover src/utils.ts:75:10\`
394
+ - \`/hover src/utils.ts:42:5\`
234
395
 
235
- <command-name>check-type</command-name>
396
+ <command-name>hover</command-name>
236
397
 
237
- Use the \`infer_type\` MCP tool to check the type:
238
- 1. Parse the arguments to extract file, optional line number, and symbol name
239
- 2. Call \`infer_type(file, name, line?)\`
240
- 3. Report the inferred signature and return type
398
+ Use the \`hover\` MCP tool to check the type:
399
+ 1. Parse the arguments to extract file, line, and column
400
+ 2. Call \`hover(file, line, column, { include_docs: true })\`
401
+ 3. Report the inferred signature, return type, and documentation
241
402
  `;
242
403
  function runSetup() {
243
404
  const homeDir = os.homedir();
@@ -248,33 +409,8 @@ function runSetup() {
248
409
  console.error(MANUAL_SETUP);
249
410
  process.exit(1);
250
411
  }
251
- let mcpOk = false;
412
+ const mcpOk = installMcpServer();
252
413
  let skillOk = false;
253
- const configFile = path3.join(claudeDir, "settings.json");
254
- try {
255
- let config = {};
256
- if (fs2.existsSync(configFile)) {
257
- config = JSON.parse(fs2.readFileSync(configFile, "utf-8"));
258
- }
259
- if (config.mcpServers?.prinfer) {
260
- console.log("[ok] MCP server already configured");
261
- mcpOk = true;
262
- } else {
263
- config.mcpServers = config.mcpServers || {};
264
- config.mcpServers.prinfer = { command: "prinfer-mcp" };
265
- fs2.writeFileSync(
266
- configFile,
267
- `${JSON.stringify(config, null, 2)}
268
- `
269
- );
270
- console.log("[ok] Added MCP server to settings.json");
271
- mcpOk = true;
272
- }
273
- } catch (err) {
274
- console.error(
275
- `[error] Failed to configure MCP server: ${err.message}`
276
- );
277
- }
278
414
  const skillsDir = path3.join(claudeDir, "skills");
279
415
  const skillFile = path3.join(skillsDir, "prefer-infer.md");
280
416
  try {
@@ -304,12 +440,16 @@ function runSetup() {
304
440
  process.exit(1);
305
441
  }
306
442
  }
307
- function parseFileArg(arg) {
308
- const match = arg.match(/^(.+):(\d+)$/);
443
+ function parsePositionArg(arg) {
444
+ const match = arg.match(/^(.+):(\d+):(\d+)$/);
309
445
  if (match) {
310
- return { file: match[1], line: Number.parseInt(match[2], 10) };
446
+ return {
447
+ file: match[1],
448
+ line: Number.parseInt(match[2], 10),
449
+ column: Number.parseInt(match[3], 10)
450
+ };
311
451
  }
312
- return { file: arg };
452
+ return null;
313
453
  }
314
454
  function parseArgs(argv) {
315
455
  const args = argv.slice(2);
@@ -321,16 +461,17 @@ function parseArgs(argv) {
321
461
  runSetup();
322
462
  return null;
323
463
  }
324
- const fileArg = args[0];
325
- const name = args[1];
326
- if (!fileArg || !name) {
464
+ const positionArg = args[0];
465
+ const parsed = parsePositionArg(positionArg);
466
+ if (!parsed) {
327
467
  console.error(
328
- "Error: Both <file> and <name> arguments are required.\n"
468
+ "Error: Position argument must be in format <file>:<line>:<column>\n"
329
469
  );
330
470
  console.log(HELP);
331
471
  process.exit(1);
332
472
  }
333
- const { file, line } = parseFileArg(fileArg);
473
+ const { file, line, column } = parsed;
474
+ const includeDocs = args.includes("--docs") || args.includes("-d");
334
475
  let project;
335
476
  const projectIdx = args.findIndex((a) => a === "--project" || a === "-p");
336
477
  if (projectIdx >= 0) {
@@ -341,7 +482,7 @@ function parseArgs(argv) {
341
482
  process.exit(1);
342
483
  }
343
484
  }
344
- return { file, name, line, project };
485
+ return { file, line, column, includeDocs, project };
345
486
  }
346
487
  function main() {
347
488
  const options = parseArgs(process.argv);
@@ -349,14 +490,21 @@ function main() {
349
490
  process.exit(0);
350
491
  }
351
492
  try {
352
- const result = inferType(options.file, options.name, {
353
- line: options.line,
493
+ const result = hover(options.file, options.line, options.column, {
494
+ include_docs: options.includeDocs,
354
495
  project: options.project
355
496
  });
356
497
  console.log(result.signature);
357
498
  if (result.returnType) {
358
499
  console.log("returns:", result.returnType);
359
500
  }
501
+ if (result.name) {
502
+ console.log("name:", result.name);
503
+ }
504
+ console.log("kind:", result.kind);
505
+ if (result.documentation) {
506
+ console.log("docs:", result.documentation);
507
+ }
360
508
  } catch (error) {
361
509
  console.error(error.message);
362
510
  process.exit(1);