@walwal-harness/cli 7.1.39 → 7.1.40
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/HR-Resource/ops/SKILL.md +10 -0
- package/HR-Resource/ops-prompt-inspector/SKILL.md +100 -0
- package/HR-Resource/ops-prompt-inspector/agents/installation-agent.json +7 -0
- package/HR-Resource/ops-prompt-inspector/assets/PromptInspector.tsx +599 -0
- package/HR-Resource/ops-prompt-inspector/references/handler-templates.md +255 -0
- package/HR-Resource/ops-prompt-inspector/scripts/discover_apis.py +509 -0
- package/HR-Resource/ops-prompt-inspector/scripts/install.sh +32 -0
- package/HR-Resource/ops-prompt-inspector/scripts/setup.py +318 -0
- package/bin/init.js +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
API Discovery Script for prompt-inspector
|
|
4
|
+
|
|
5
|
+
Scans a project directory to find:
|
|
6
|
+
1. Frontend API calls (axios, fetch, ky, custom instances, etc.)
|
|
7
|
+
2. Backend API endpoints (Express, FastAPI, NestJS, Next.js API routes, etc.)
|
|
8
|
+
3. OpenAPI/Swagger specification files
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
python discover_apis.py <project_path> [--output json|markdown]
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
16
|
+
import json
|
|
17
|
+
import argparse
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from dataclasses import dataclass, asdict
|
|
20
|
+
from typing import Optional, Set
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class APICall:
|
|
24
|
+
"""Represents a discovered API call or endpoint"""
|
|
25
|
+
type: str # 'frontend', 'backend', or 'spec'
|
|
26
|
+
method: str # GET, POST, PUT, DELETE, PATCH
|
|
27
|
+
path: str # API path/URL
|
|
28
|
+
file: str # Source file
|
|
29
|
+
line: int # Line number
|
|
30
|
+
library: str # axios, fetch, express, openapi, etc.
|
|
31
|
+
instance_name: Optional[str] = None # Custom instance name if applicable
|
|
32
|
+
function_name: Optional[str] = None # Function/handler name if available
|
|
33
|
+
|
|
34
|
+
# HTTP methods to detect
|
|
35
|
+
HTTP_METHODS = {'get', 'post', 'put', 'delete', 'patch', 'head', 'options'}
|
|
36
|
+
HTTP_METHODS_UPPER = {m.upper() for m in HTTP_METHODS}
|
|
37
|
+
|
|
38
|
+
# Known HTTP client libraries and their creation patterns
|
|
39
|
+
HTTP_CLIENT_CREATORS = {
|
|
40
|
+
'axios': [r'axios\.create\s*\(', r'import\s+axios\s+from'],
|
|
41
|
+
'ky': [r'ky\.create\s*\(', r'import\s+ky\s+from'],
|
|
42
|
+
'got': [r'got\.extend\s*\(', r'import\s+got\s+from'],
|
|
43
|
+
'superagent': [r'import\s+superagent\s+from', r'require\s*\(\s*[\'"]superagent[\'"]\s*\)'],
|
|
44
|
+
'ofetch': [r'ofetch\.create\s*\(', r'\$fetch', r'import.*from\s*[\'"]ofetch[\'"]'],
|
|
45
|
+
'wretch': [r'wretch\s*\(', r'import\s+wretch\s+from'],
|
|
46
|
+
'redaxios': [r'import\s+redaxios\s+from', r'redaxios\.create\s*\('],
|
|
47
|
+
'apisauce': [r'create\s*\(\s*\{[^}]*baseURL', r'import.*from\s*[\'"]apisauce[\'"]'],
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# Patterns to find custom HTTP client instance declarations
|
|
51
|
+
INSTANCE_DECLARATION_PATTERNS = [
|
|
52
|
+
# export const apiClient = axios.create(...)
|
|
53
|
+
r'(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:axios|ky|got|superagent|wretch|redaxios)(?:\.create|\.extend)?\s*\(',
|
|
54
|
+
# const api = axios.create(...)
|
|
55
|
+
r'(?:const|let|var)\s+(\w+)\s*=\s*\w+\.create\s*\(\s*\{[^}]*baseURL',
|
|
56
|
+
# export const http = new HttpClient(...)
|
|
57
|
+
r'(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*new\s+\w*(?:Http|Api|Client|Service)\w*\s*\(',
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
# Common HTTP client instance names (fallback detection)
|
|
61
|
+
COMMON_INSTANCE_NAMES = {
|
|
62
|
+
'api', 'apiClient', 'client', 'http', 'httpClient',
|
|
63
|
+
'request', 'requests', 'axios', 'instance', 'service',
|
|
64
|
+
'fetcher', 'apiService', 'httpService', 'restClient',
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
IGNORED_DIRS = {
|
|
68
|
+
'node_modules', '.git', '.next', 'dist', 'build',
|
|
69
|
+
'__pycache__', '.venv', 'venv', 'coverage', '.turbo'
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
FRONTEND_EXTENSIONS = {'.js', '.jsx', '.ts', '.tsx', '.mjs'}
|
|
73
|
+
BACKEND_EXTENSIONS = {'.js', '.ts', '.py', '.mjs'}
|
|
74
|
+
SPEC_EXTENSIONS = {'.json', '.yaml', '.yml'}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def should_skip_dir(dir_name: str) -> bool:
|
|
78
|
+
return dir_name in IGNORED_DIRS or dir_name.startswith('.')
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def find_http_client_instances(content: str) -> Set[str]:
|
|
82
|
+
"""Find custom HTTP client instance names in the file"""
|
|
83
|
+
instances = set()
|
|
84
|
+
|
|
85
|
+
for pattern in INSTANCE_DECLARATION_PATTERNS:
|
|
86
|
+
for match in re.finditer(pattern, content, re.MULTILINE):
|
|
87
|
+
if match.group(1):
|
|
88
|
+
instances.add(match.group(1))
|
|
89
|
+
|
|
90
|
+
return instances
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def extract_url_from_match(match_str: str) -> Optional[str]:
|
|
94
|
+
"""Extract URL/path from various string formats"""
|
|
95
|
+
# Remove TypeScript generics like <T> or <ResponseType>
|
|
96
|
+
match_str = re.sub(r'<[^>]+>\s*', '', match_str)
|
|
97
|
+
|
|
98
|
+
# Handle template literals: `${baseUrl}/path` -> extract /path part
|
|
99
|
+
template_match = re.search(r'`[^`]*(/[^`\$]+)`', match_str)
|
|
100
|
+
if template_match:
|
|
101
|
+
return template_match.group(1)
|
|
102
|
+
|
|
103
|
+
# Handle simple strings: '/api/users' or "/api/users"
|
|
104
|
+
string_match = re.search(r'[\'"`](/[^\'"`\s]+)[\'"`]', match_str)
|
|
105
|
+
if string_match:
|
|
106
|
+
return string_match.group(1)
|
|
107
|
+
|
|
108
|
+
# Handle full URLs
|
|
109
|
+
url_match = re.search(r'[\'"`](https?://[^\'"`\s]+)[\'"`]', match_str)
|
|
110
|
+
if url_match:
|
|
111
|
+
return url_match.group(1)
|
|
112
|
+
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def find_frontend_apis(file_path: Path, content: str) -> list[APICall]:
|
|
117
|
+
"""Find frontend API calls in a file"""
|
|
118
|
+
apis = []
|
|
119
|
+
|
|
120
|
+
# Find custom instances in this file
|
|
121
|
+
custom_instances = find_http_client_instances(content)
|
|
122
|
+
|
|
123
|
+
# Build dynamic pattern for HTTP method calls
|
|
124
|
+
# Matches: instanceName.method<T>('/path', ...) or instanceName.method('/path', ...)
|
|
125
|
+
all_instances = custom_instances | COMMON_INSTANCE_NAMES | {'axios', 'ky', 'got', 'superagent', 'fetch'}
|
|
126
|
+
|
|
127
|
+
# Pattern 1: instance.method<T?>('/path') - covers axios, ky, got, custom instances
|
|
128
|
+
# Handles: apiClient.post<LoginResponse>('/v1/accounts/login/email', data)
|
|
129
|
+
methods_pattern = '|'.join(HTTP_METHODS)
|
|
130
|
+
instance_pattern = '|'.join(re.escape(name) for name in all_instances)
|
|
131
|
+
|
|
132
|
+
# Generic pattern for any potential HTTP client call
|
|
133
|
+
# Match: word.httpMethod<optional generic>( string or template literal
|
|
134
|
+
generic_method_pattern = rf'(\w+)\.({methods_pattern})\s*(?:<[^>]*>)?\s*\(\s*([\'"`][^)]+)'
|
|
135
|
+
|
|
136
|
+
for match in re.finditer(generic_method_pattern, content, re.IGNORECASE):
|
|
137
|
+
instance_name = match.group(1)
|
|
138
|
+
method = match.group(2).upper()
|
|
139
|
+
url_part = match.group(3)
|
|
140
|
+
|
|
141
|
+
# Skip if it doesn't look like an HTTP client call
|
|
142
|
+
if instance_name.lower() in {'console', 'math', 'array', 'object', 'string', 'promise', 'window', 'document'}:
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
url = extract_url_from_match(url_part)
|
|
146
|
+
if not url:
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
line_num = content[:match.start()].count('\n') + 1
|
|
150
|
+
|
|
151
|
+
# Determine library
|
|
152
|
+
library = 'unknown'
|
|
153
|
+
if instance_name in custom_instances:
|
|
154
|
+
library = 'custom-instance'
|
|
155
|
+
elif instance_name.lower() in {'axios'}:
|
|
156
|
+
library = 'axios'
|
|
157
|
+
elif instance_name.lower() in {'ky'}:
|
|
158
|
+
library = 'ky'
|
|
159
|
+
elif instance_name.lower() in {'got'}:
|
|
160
|
+
library = 'got'
|
|
161
|
+
elif instance_name in COMMON_INSTANCE_NAMES:
|
|
162
|
+
library = 'http-client'
|
|
163
|
+
|
|
164
|
+
apis.append(APICall(
|
|
165
|
+
type='frontend',
|
|
166
|
+
method=method,
|
|
167
|
+
path=url,
|
|
168
|
+
file=str(file_path),
|
|
169
|
+
line=line_num,
|
|
170
|
+
library=library,
|
|
171
|
+
instance_name=instance_name if instance_name not in {'axios', 'ky', 'got', 'fetch'} else None
|
|
172
|
+
))
|
|
173
|
+
|
|
174
|
+
# Pattern 2: fetch('/path', { method: 'POST' }) or fetch('/path')
|
|
175
|
+
fetch_pattern = r'fetch\s*\(\s*([\'"`][^\'"`]+[\'"`])\s*(?:,\s*\{[^}]*method:\s*[\'"`](\w+)[\'"`])?'
|
|
176
|
+
for match in re.finditer(fetch_pattern, content, re.IGNORECASE):
|
|
177
|
+
url = extract_url_from_match(match.group(1))
|
|
178
|
+
if not url:
|
|
179
|
+
continue
|
|
180
|
+
method = match.group(2).upper() if match.group(2) else 'GET'
|
|
181
|
+
line_num = content[:match.start()].count('\n') + 1
|
|
182
|
+
|
|
183
|
+
apis.append(APICall(
|
|
184
|
+
type='frontend',
|
|
185
|
+
method=method,
|
|
186
|
+
path=url,
|
|
187
|
+
file=str(file_path),
|
|
188
|
+
line=line_num,
|
|
189
|
+
library='fetch'
|
|
190
|
+
))
|
|
191
|
+
|
|
192
|
+
# Pattern 3: useSWR('/api/users') or useQuery(['/api/users'])
|
|
193
|
+
swr_pattern = r'useSWR\s*\(\s*[\'"`]([^\'"`]+)[\'"`]'
|
|
194
|
+
for match in re.finditer(swr_pattern, content):
|
|
195
|
+
url = match.group(1)
|
|
196
|
+
if url.startswith('/') or url.startswith('http'):
|
|
197
|
+
line_num = content[:match.start()].count('\n') + 1
|
|
198
|
+
apis.append(APICall(
|
|
199
|
+
type='frontend',
|
|
200
|
+
method='GET',
|
|
201
|
+
path=url,
|
|
202
|
+
file=str(file_path),
|
|
203
|
+
line=line_num,
|
|
204
|
+
library='swr'
|
|
205
|
+
))
|
|
206
|
+
|
|
207
|
+
# Pattern 4: useQuery with fetch
|
|
208
|
+
query_pattern = r'useQuery\s*\([^)]*fetch\s*\(\s*[\'"`]([^\'"`]+)[\'"`]'
|
|
209
|
+
for match in re.finditer(query_pattern, content):
|
|
210
|
+
url = match.group(1)
|
|
211
|
+
if url.startswith('/') or url.startswith('http'):
|
|
212
|
+
line_num = content[:match.start()].count('\n') + 1
|
|
213
|
+
apis.append(APICall(
|
|
214
|
+
type='frontend',
|
|
215
|
+
method='GET',
|
|
216
|
+
path=url,
|
|
217
|
+
file=str(file_path),
|
|
218
|
+
line=line_num,
|
|
219
|
+
library='react-query'
|
|
220
|
+
))
|
|
221
|
+
|
|
222
|
+
return apis
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def find_backend_apis(file_path: Path, content: str) -> list[APICall]:
|
|
226
|
+
"""Find backend API endpoints in a file"""
|
|
227
|
+
apis = []
|
|
228
|
+
|
|
229
|
+
# Detect Next.js API routes from file path
|
|
230
|
+
if '/app/api/' in str(file_path) or '/pages/api/' in str(file_path):
|
|
231
|
+
path_str = str(file_path)
|
|
232
|
+
if '/app/api/' in path_str:
|
|
233
|
+
route = path_str.split('/app/api/')[-1]
|
|
234
|
+
route = '/api/' + route.replace('/route.ts', '').replace('/route.js', '')
|
|
235
|
+
else:
|
|
236
|
+
route = path_str.split('/pages/api/')[-1]
|
|
237
|
+
route = '/api/' + route.replace('.ts', '').replace('.js', '')
|
|
238
|
+
|
|
239
|
+
# Find exported HTTP methods
|
|
240
|
+
for match in re.finditer(r'export\s+(async\s+)?function\s+(GET|POST|PUT|DELETE|PATCH)', content):
|
|
241
|
+
method = match.group(2)
|
|
242
|
+
line_num = content[:match.start()].count('\n') + 1
|
|
243
|
+
apis.append(APICall(
|
|
244
|
+
type='backend',
|
|
245
|
+
method=method,
|
|
246
|
+
path=route.replace('[', '{').replace(']', '}'),
|
|
247
|
+
file=str(file_path),
|
|
248
|
+
line=line_num,
|
|
249
|
+
library='nextjs-api'
|
|
250
|
+
))
|
|
251
|
+
|
|
252
|
+
for match in re.finditer(r'export\s+const\s+(GET|POST|PUT|DELETE|PATCH)\s*=', content):
|
|
253
|
+
method = match.group(1)
|
|
254
|
+
line_num = content[:match.start()].count('\n') + 1
|
|
255
|
+
apis.append(APICall(
|
|
256
|
+
type='backend',
|
|
257
|
+
method=method,
|
|
258
|
+
path=route.replace('[', '{').replace(']', '}'),
|
|
259
|
+
file=str(file_path),
|
|
260
|
+
line=line_num,
|
|
261
|
+
library='nextjs-api'
|
|
262
|
+
))
|
|
263
|
+
|
|
264
|
+
# Express/Hono style: app.get('/path', ...) or router.post('/path', ...)
|
|
265
|
+
express_pattern = r'(app|router)\.(get|post|put|delete|patch)\s*\(\s*[\'"`]([^\'"`]+)[\'"`]'
|
|
266
|
+
for match in re.finditer(express_pattern, content, re.IGNORECASE):
|
|
267
|
+
method = match.group(2).upper()
|
|
268
|
+
path = match.group(3)
|
|
269
|
+
line_num = content[:match.start()].count('\n') + 1
|
|
270
|
+
apis.append(APICall(
|
|
271
|
+
type='backend',
|
|
272
|
+
method=method,
|
|
273
|
+
path=path,
|
|
274
|
+
file=str(file_path),
|
|
275
|
+
line=line_num,
|
|
276
|
+
library='express'
|
|
277
|
+
))
|
|
278
|
+
|
|
279
|
+
# FastAPI style: @app.get('/path')
|
|
280
|
+
fastapi_pattern = r'@(?:app|router)\.(get|post|put|delete|patch)\s*\(\s*[\'"`]([^\'"`]+)[\'"`]'
|
|
281
|
+
for match in re.finditer(fastapi_pattern, content, re.IGNORECASE):
|
|
282
|
+
method = match.group(1).upper()
|
|
283
|
+
path = match.group(2)
|
|
284
|
+
line_num = content[:match.start()].count('\n') + 1
|
|
285
|
+
apis.append(APICall(
|
|
286
|
+
type='backend',
|
|
287
|
+
method=method,
|
|
288
|
+
path=path,
|
|
289
|
+
file=str(file_path),
|
|
290
|
+
line=line_num,
|
|
291
|
+
library='fastapi'
|
|
292
|
+
))
|
|
293
|
+
|
|
294
|
+
# NestJS style: @Get('/path'), @Post(), etc.
|
|
295
|
+
nestjs_pattern = r'@(Get|Post|Put|Delete|Patch)\s*\(\s*[\'"`]?([^\'"`\)]*)[\'"`]?\s*\)'
|
|
296
|
+
for match in re.finditer(nestjs_pattern, content):
|
|
297
|
+
method = match.group(1).upper()
|
|
298
|
+
path = match.group(2) if match.group(2) else '/'
|
|
299
|
+
line_num = content[:match.start()].count('\n') + 1
|
|
300
|
+
apis.append(APICall(
|
|
301
|
+
type='backend',
|
|
302
|
+
method=method,
|
|
303
|
+
path=path,
|
|
304
|
+
file=str(file_path),
|
|
305
|
+
line=line_num,
|
|
306
|
+
library='nestjs'
|
|
307
|
+
))
|
|
308
|
+
|
|
309
|
+
return apis
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def find_openapi_specs(file_path: Path, content: str) -> list[APICall]:
|
|
313
|
+
"""Parse OpenAPI/Swagger specification files"""
|
|
314
|
+
apis = []
|
|
315
|
+
|
|
316
|
+
try:
|
|
317
|
+
if file_path.suffix == '.json':
|
|
318
|
+
spec = json.loads(content)
|
|
319
|
+
else:
|
|
320
|
+
# For YAML, try to parse as JSON-like structure
|
|
321
|
+
# In production, you'd use pyyaml
|
|
322
|
+
return apis
|
|
323
|
+
|
|
324
|
+
# Check if this is an OpenAPI/Swagger spec
|
|
325
|
+
if not ('openapi' in spec or 'swagger' in spec):
|
|
326
|
+
return apis
|
|
327
|
+
|
|
328
|
+
# Get base path
|
|
329
|
+
base_path = ''
|
|
330
|
+
if 'basePath' in spec: # Swagger 2.0
|
|
331
|
+
base_path = spec['basePath']
|
|
332
|
+
elif 'servers' in spec and spec['servers']: # OpenAPI 3.x
|
|
333
|
+
server_url = spec['servers'][0].get('url', '')
|
|
334
|
+
# Extract path from URL if present
|
|
335
|
+
if '/' in server_url.replace('://', ''):
|
|
336
|
+
base_path = '/' + server_url.split('/', 3)[-1] if server_url.count('/') > 2 else ''
|
|
337
|
+
|
|
338
|
+
# Parse paths
|
|
339
|
+
paths = spec.get('paths', {})
|
|
340
|
+
for path, methods in paths.items():
|
|
341
|
+
if not isinstance(methods, dict):
|
|
342
|
+
continue
|
|
343
|
+
|
|
344
|
+
full_path = base_path + path
|
|
345
|
+
|
|
346
|
+
for method, details in methods.items():
|
|
347
|
+
if method.upper() not in HTTP_METHODS_UPPER:
|
|
348
|
+
continue
|
|
349
|
+
|
|
350
|
+
summary = ''
|
|
351
|
+
if isinstance(details, dict):
|
|
352
|
+
summary = details.get('summary', details.get('operationId', ''))
|
|
353
|
+
|
|
354
|
+
apis.append(APICall(
|
|
355
|
+
type='spec',
|
|
356
|
+
method=method.upper(),
|
|
357
|
+
path=full_path,
|
|
358
|
+
file=str(file_path),
|
|
359
|
+
line=1,
|
|
360
|
+
library='openapi',
|
|
361
|
+
function_name=summary if summary else None
|
|
362
|
+
))
|
|
363
|
+
|
|
364
|
+
except (json.JSONDecodeError, KeyError, TypeError):
|
|
365
|
+
pass
|
|
366
|
+
|
|
367
|
+
return apis
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def scan_directory(project_path: Path) -> list[APICall]:
|
|
371
|
+
"""Scan a directory for all API calls and endpoints"""
|
|
372
|
+
all_apis = []
|
|
373
|
+
|
|
374
|
+
for root, dirs, files in os.walk(project_path):
|
|
375
|
+
# Filter out ignored directories
|
|
376
|
+
dirs[:] = [d for d in dirs if not should_skip_dir(d)]
|
|
377
|
+
|
|
378
|
+
for file in files:
|
|
379
|
+
file_path = Path(root) / file
|
|
380
|
+
suffix = file_path.suffix.lower()
|
|
381
|
+
|
|
382
|
+
if suffix not in FRONTEND_EXTENSIONS | BACKEND_EXTENSIONS | SPEC_EXTENSIONS:
|
|
383
|
+
continue
|
|
384
|
+
|
|
385
|
+
try:
|
|
386
|
+
content = file_path.read_text(encoding='utf-8')
|
|
387
|
+
except (UnicodeDecodeError, PermissionError):
|
|
388
|
+
continue
|
|
389
|
+
|
|
390
|
+
# Check frontend patterns
|
|
391
|
+
if suffix in FRONTEND_EXTENSIONS:
|
|
392
|
+
all_apis.extend(find_frontend_apis(file_path, content))
|
|
393
|
+
|
|
394
|
+
# Check backend patterns
|
|
395
|
+
if suffix in BACKEND_EXTENSIONS:
|
|
396
|
+
all_apis.extend(find_backend_apis(file_path, content))
|
|
397
|
+
|
|
398
|
+
# Check OpenAPI specs
|
|
399
|
+
if suffix in SPEC_EXTENSIONS:
|
|
400
|
+
all_apis.extend(find_openapi_specs(file_path, content))
|
|
401
|
+
|
|
402
|
+
# Deduplicate
|
|
403
|
+
seen = set()
|
|
404
|
+
unique_apis = []
|
|
405
|
+
for api in all_apis:
|
|
406
|
+
key = (api.type, api.method, api.path, api.file, api.line)
|
|
407
|
+
if key not in seen:
|
|
408
|
+
seen.add(key)
|
|
409
|
+
unique_apis.append(api)
|
|
410
|
+
|
|
411
|
+
return unique_apis
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def format_markdown(apis: list[APICall], project_path: str) -> str:
|
|
415
|
+
"""Format API list as markdown"""
|
|
416
|
+
frontend = [a for a in apis if a.type == 'frontend']
|
|
417
|
+
backend = [a for a in apis if a.type == 'backend']
|
|
418
|
+
specs = [a for a in apis if a.type == 'spec']
|
|
419
|
+
|
|
420
|
+
output = f"# API Discovery Report\n\n"
|
|
421
|
+
output += f"**Project:** `{project_path}`\n"
|
|
422
|
+
output += f"**Total APIs Found:** {len(apis)}\n"
|
|
423
|
+
output += f"- Frontend Calls: {len(frontend)}\n"
|
|
424
|
+
output += f"- Backend Endpoints: {len(backend)}\n"
|
|
425
|
+
output += f"- OpenAPI Specs: {len(specs)}\n\n"
|
|
426
|
+
|
|
427
|
+
if specs:
|
|
428
|
+
output += "## OpenAPI Specifications\n\n"
|
|
429
|
+
output += "| Method | Path | Summary | File |\n"
|
|
430
|
+
output += "|--------|------|---------|------|\n"
|
|
431
|
+
for api in sorted(specs, key=lambda x: (x.file, x.path, x.method)):
|
|
432
|
+
rel_path = os.path.relpath(api.file, project_path)
|
|
433
|
+
summary = api.function_name or '-'
|
|
434
|
+
output += f"| `{api.method}` | `{api.path}` | {summary} | `{rel_path}` |\n"
|
|
435
|
+
output += "\n"
|
|
436
|
+
|
|
437
|
+
if backend:
|
|
438
|
+
output += "## Backend Endpoints\n\n"
|
|
439
|
+
output += "| Method | Path | Library | File | Line |\n"
|
|
440
|
+
output += "|--------|------|---------|------|------|\n"
|
|
441
|
+
for api in sorted(backend, key=lambda x: (x.path, x.method)):
|
|
442
|
+
rel_path = os.path.relpath(api.file, project_path)
|
|
443
|
+
output += f"| `{api.method}` | `{api.path}` | {api.library} | `{rel_path}` | {api.line} |\n"
|
|
444
|
+
output += "\n"
|
|
445
|
+
|
|
446
|
+
if frontend:
|
|
447
|
+
output += "## Frontend API Calls\n\n"
|
|
448
|
+
output += "| Method | Path | Library | Instance | File | Line |\n"
|
|
449
|
+
output += "|--------|------|---------|----------|------|------|\n"
|
|
450
|
+
for api in sorted(frontend, key=lambda x: (x.path, x.method)):
|
|
451
|
+
rel_path = os.path.relpath(api.file, project_path)
|
|
452
|
+
instance = api.instance_name or '-'
|
|
453
|
+
output += f"| `{api.method}` | `{api.path}` | {api.library} | {instance} | `{rel_path}` | {api.line} |\n"
|
|
454
|
+
|
|
455
|
+
return output
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def format_json_for_inspector(apis: list[APICall]) -> list[dict]:
|
|
459
|
+
"""Format APIs for PromptInspector component consumption"""
|
|
460
|
+
result = []
|
|
461
|
+
seen_paths = set()
|
|
462
|
+
|
|
463
|
+
for api in apis:
|
|
464
|
+
# Create unique ID
|
|
465
|
+
api_id = f"{api.method}_{api.path}".replace('/', '_').replace('{', '').replace('}', '')
|
|
466
|
+
|
|
467
|
+
# Skip duplicates (same method + path)
|
|
468
|
+
key = (api.method, api.path)
|
|
469
|
+
if key in seen_paths:
|
|
470
|
+
continue
|
|
471
|
+
seen_paths.add(key)
|
|
472
|
+
|
|
473
|
+
result.append({
|
|
474
|
+
'id': api_id,
|
|
475
|
+
'method': api.method,
|
|
476
|
+
'path': api.path,
|
|
477
|
+
'description': api.function_name or f"{api.library} - {os.path.basename(api.file)}"
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
return result
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def main():
|
|
484
|
+
parser = argparse.ArgumentParser(description='Discover APIs in a project')
|
|
485
|
+
parser.add_argument('project_path', help='Path to the project directory')
|
|
486
|
+
parser.add_argument('--output', choices=['json', 'markdown', 'inspector'], default='markdown',
|
|
487
|
+
help='Output format (default: markdown, inspector for PromptInspector format)')
|
|
488
|
+
args = parser.parse_args()
|
|
489
|
+
|
|
490
|
+
project_path = Path(args.project_path).resolve()
|
|
491
|
+
|
|
492
|
+
if not project_path.exists():
|
|
493
|
+
print(f"Error: Path '{project_path}' does not exist")
|
|
494
|
+
return 1
|
|
495
|
+
|
|
496
|
+
apis = scan_directory(project_path)
|
|
497
|
+
|
|
498
|
+
if args.output == 'json':
|
|
499
|
+
print(json.dumps([asdict(api) for api in apis], indent=2))
|
|
500
|
+
elif args.output == 'inspector':
|
|
501
|
+
print(json.dumps(format_json_for_inspector(apis), indent=2))
|
|
502
|
+
else:
|
|
503
|
+
print(format_markdown(apis, str(project_path)))
|
|
504
|
+
|
|
505
|
+
return 0
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
if __name__ == '__main__':
|
|
509
|
+
exit(main())
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Prompt Inspector - One-command installer
|
|
3
|
+
# Usage: ./install.sh <project_path>
|
|
4
|
+
|
|
5
|
+
set -e
|
|
6
|
+
|
|
7
|
+
PROJECT_PATH="${1:-.}"
|
|
8
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
9
|
+
SKILL_DIR="$(dirname "$SCRIPT_DIR")"
|
|
10
|
+
|
|
11
|
+
echo "═══════════════════════════════════════════════════════════════"
|
|
12
|
+
echo " 🔧 Prompt Inspector Installer"
|
|
13
|
+
echo "═══════════════════════════════════════════════════════════════"
|
|
14
|
+
echo ""
|
|
15
|
+
|
|
16
|
+
# Run setup
|
|
17
|
+
echo "📦 Step 1/2: Installing component..."
|
|
18
|
+
python3 "$SCRIPT_DIR/setup.py" "$PROJECT_PATH" --force
|
|
19
|
+
|
|
20
|
+
echo ""
|
|
21
|
+
echo "🔍 Step 2/2: Discovering APIs..."
|
|
22
|
+
python3 "$SCRIPT_DIR/discover_apis.py" "$PROJECT_PATH"
|
|
23
|
+
|
|
24
|
+
echo ""
|
|
25
|
+
echo "═══════════════════════════════════════════════════════════════"
|
|
26
|
+
echo " ✅ Installation Complete!"
|
|
27
|
+
echo ""
|
|
28
|
+
echo " Next steps:"
|
|
29
|
+
echo " 1. Start your dev server (npm run dev)"
|
|
30
|
+
echo " 2. Look for the toolbar at bottom-right"
|
|
31
|
+
echo " 3. Click 'Select Element' to start binding APIs"
|
|
32
|
+
echo "═══════════════════════════════════════════════════════════════"
|