@ranger-testing/ranger-cli 1.0.10 → 1.0.13

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.
Files changed (66) hide show
  1. package/README.md +111 -0
  2. package/build/cli.js +290 -8
  3. package/build/cli.js.map +1 -1
  4. package/build/commands/addEnv.js +1 -1
  5. package/build/commands/addEnv.js.map +1 -1
  6. package/build/commands/clean.js +1 -1
  7. package/build/commands/clean.js.map +1 -1
  8. package/build/commands/dataMcpServer.js +1 -1
  9. package/build/commands/dataMcpServer.js.map +1 -1
  10. package/build/commands/env.js +46 -0
  11. package/build/commands/env.js.map +1 -0
  12. package/build/commands/feature.js +494 -0
  13. package/build/commands/feature.js.map +1 -0
  14. package/build/commands/index.js +1 -0
  15. package/build/commands/index.js.map +1 -1
  16. package/build/commands/skillup.js +68 -16
  17. package/build/commands/skillup.js.map +1 -1
  18. package/build/commands/start.js +1 -1
  19. package/build/commands/start.js.map +1 -1
  20. package/build/commands/status.js +42 -11
  21. package/build/commands/status.js.map +1 -1
  22. package/build/commands/update.js +29 -16
  23. package/build/commands/update.js.map +1 -1
  24. package/build/commands/updateEnv.js +1 -1
  25. package/build/commands/updateEnv.js.map +1 -1
  26. package/build/commands/useEnv.js +1 -1
  27. package/build/commands/useEnv.js.map +1 -1
  28. package/build/commands/utils/browserSessionsApi.js +1 -1
  29. package/build/commands/utils/browserSessionsApi.js.map +1 -1
  30. package/build/commands/utils/claudeConfig.js +73 -0
  31. package/build/commands/utils/claudeConfig.js.map +1 -0
  32. package/build/commands/utils/cliSecret.js +1 -1
  33. package/build/commands/utils/environment.js +69 -0
  34. package/build/commands/utils/environment.js.map +1 -0
  35. package/build/commands/utils/featureApi.js +190 -0
  36. package/build/commands/utils/featureApi.js.map +1 -0
  37. package/build/commands/utils/featureReportGenerator.js +170 -0
  38. package/build/commands/utils/featureReportGenerator.js.map +1 -0
  39. package/build/commands/utils/keychain.js +1 -1
  40. package/build/commands/utils/localAgentInstallationsApi.js +1 -1
  41. package/build/commands/utils/localAgentInstallationsApi.js.map +1 -1
  42. package/build/commands/utils/mcpConfig.js +1 -1
  43. package/build/commands/utils/settings.js +2 -2
  44. package/build/commands/utils/settings.js.map +1 -1
  45. package/build/commands/utils/skills.js +1 -1
  46. package/build/commands/utils/skills.js.map +1 -1
  47. package/build/commands/verifyFeature.js +451 -0
  48. package/build/commands/verifyFeature.js.map +1 -0
  49. package/build/commands/verifyInBrowser.js +1 -1
  50. package/build/commands/verifyInBrowser.js.map +1 -1
  51. package/build/skills/feature-tracker/SKILL.md +185 -0
  52. package/build/skills/feature-tracker/create.md +105 -0
  53. package/build/skills/feature-tracker/manage.md +145 -0
  54. package/build/skills/feature-tracker/report.md +159 -0
  55. package/build/skills/feature-tracker/start.md +93 -0
  56. package/build/skills/feature-tracker/verify.md +143 -0
  57. package/package.json +23 -20
  58. package/build/agents/bug-basher.md +0 -259
  59. package/build/agents/e2e-test-recommender.md +0 -164
  60. package/build/agents/quality-advocate.md +0 -164
  61. package/build/agents/ui-verifier.md +0 -100
  62. package/build/commands/addApp.js +0 -21
  63. package/build/commands/initAgents.js +0 -84
  64. package/build/commands/utils/agents.js +0 -45
  65. package/build/index.js +0 -436
  66. package/build/test-auth.js +0 -13
package/build/index.js DELETED
@@ -1,436 +0,0 @@
1
- #!/usr/bin/env node
2
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
- import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
4
- import { z } from 'zod';
5
- import { createServer } from 'http';
6
- const RANGER_API_BASE = process.env.RANGER_API_URL || 'http://localhost:8080';
7
- const PORT = parseInt(process.env.PORT || '8080', 10);
8
- // Tool classification - read tools can be used with mcp:read scope
9
- const READ_TOOLS = ['get_test_runs', 'get_active_tests'];
10
- const tokenInfoCache = new Map();
11
- const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
12
- // Create API request function bound to a specific token
13
- function createApiClient(token) {
14
- console.error(`[API Client] Created with customer token: ${token ? token.substring(0, 10) + '...' : 'none'}`);
15
- // All requests now use customer token
16
- const apiRequest = async (endpoint, options = {}) => {
17
- const url = `${RANGER_API_BASE}${endpoint}`;
18
- const headers = {
19
- 'Content-Type': 'application/json',
20
- ...(token ? { Authorization: `Bearer ${token}` } : {}),
21
- ...options.headers,
22
- };
23
- console.error(`[API Request] ${options.method || 'GET'} ${url}`);
24
- const response = await fetch(url, {
25
- ...options,
26
- headers,
27
- });
28
- if (!response.ok) {
29
- const body = await response.text();
30
- console.error(`[API Error] ${response.status} ${response.statusText}: ${body}`);
31
- throw new Error(`API request failed: ${response.status} ${response.statusText}`);
32
- }
33
- return response.json();
34
- };
35
- const getTokenInfo = async () => {
36
- if (!token) {
37
- throw new Error('No authorization token provided');
38
- }
39
- // Check cache
40
- const cached = tokenInfoCache.get(token);
41
- if (cached && cached.expiresAt > Date.now()) {
42
- return { orgId: cached.orgId, scope: cached.scope, apiKeyId: cached.apiKeyId };
43
- }
44
- // Fetch from API using customer token
45
- const data = await apiRequest('/api/v1/me');
46
- const orgId = data.organizationId || data.organization?.id;
47
- const scope = (data.scope || 'api:all');
48
- const apiKeyId = data.apiKeyId;
49
- if (!orgId) {
50
- throw new Error('Could not determine organization ID from token');
51
- }
52
- if (!apiKeyId) {
53
- throw new Error('Could not determine API key ID from token');
54
- }
55
- // Cache the result
56
- tokenInfoCache.set(token, { orgId, scope, apiKeyId, expiresAt: Date.now() + CACHE_TTL_MS });
57
- return { orgId, scope, apiKeyId };
58
- };
59
- // Helper to log MCP tool calls
60
- const logToolCall = async (params) => {
61
- try {
62
- await apiRequest('/api/v1/mcp/tool-calls', {
63
- method: 'POST',
64
- body: JSON.stringify(params),
65
- });
66
- }
67
- catch (error) {
68
- // Log but don't fail the tool call if logging fails
69
- console.error(`[MCP] Failed to log tool call: ${error}`);
70
- }
71
- };
72
- return { apiRequest, getTokenInfo, logToolCall };
73
- }
74
- // Register all tools on an MCP server instance
75
- async function registerTools(server, token) {
76
- const { apiRequest, getTokenInfo, logToolCall } = createApiClient(token);
77
- // Get token info including scope
78
- let tokenInfo;
79
- try {
80
- tokenInfo = await getTokenInfo();
81
- }
82
- catch (error) {
83
- console.error(`[MCP] Failed to get token info: ${error}`);
84
- // Register an error-only tool if we can't get token info
85
- server.registerTool('error', {
86
- description: 'Error tool - authentication failed',
87
- inputSchema: {},
88
- }, async () => ({
89
- content: [{ type: 'text', text: `Authentication failed: ${error}` }],
90
- }));
91
- return;
92
- }
93
- const { orgId, scope } = tokenInfo;
94
- // Check if scope allows MCP access
95
- if (scope === 'api:all') {
96
- console.error(`[MCP] Access denied: API key scope 'api:all' does not allow MCP access`);
97
- server.registerTool('access_denied', {
98
- description: 'Access denied - API key scope does not allow MCP access',
99
- inputSchema: {},
100
- }, async () => ({
101
- content: [{
102
- type: 'text',
103
- text: 'Access denied: Your API key has scope "api:all" which does not allow MCP access. Please create an API key with scope "mcp:read" or "mcp:write" to use the MCP server.'
104
- }],
105
- }));
106
- return;
107
- }
108
- // Determine which tools to register based on scope
109
- const canRegisterTool = (toolName) => {
110
- if (scope === 'mcp:write') {
111
- return true; // Write scope can use all tools
112
- }
113
- if (scope === 'mcp:read') {
114
- return READ_TOOLS.includes(toolName);
115
- }
116
- return false;
117
- };
118
- // Helper to wrap tool handlers with logging
119
- const wrapWithLogging = (toolName, handler) => {
120
- return async (args) => {
121
- const startTime = Date.now();
122
- try {
123
- const result = await handler(args);
124
- const durationMs = Date.now() - startTime;
125
- await logToolCall({
126
- toolName,
127
- inputArgs: args,
128
- success: true,
129
- durationMs,
130
- });
131
- return result;
132
- }
133
- catch (error) {
134
- const durationMs = Date.now() - startTime;
135
- const errorMessage = error instanceof Error ? error.message : String(error);
136
- await logToolCall({
137
- toolName,
138
- inputArgs: args,
139
- success: false,
140
- errorMessage,
141
- durationMs,
142
- });
143
- throw error;
144
- }
145
- };
146
- };
147
- // Register tool: Get test runs
148
- if (canRegisterTool('get_test_runs')) {
149
- server.registerTool('get_test_runs', {
150
- description: 'Get test runs for the organization associated with your API token',
151
- inputSchema: {
152
- limit: z
153
- .number()
154
- .optional()
155
- .describe('Maximum number of runs to return (default: 10)'),
156
- offset: z
157
- .number()
158
- .optional()
159
- .describe('Pagination offset (default: 0)'),
160
- },
161
- }, wrapWithLogging('get_test_runs', async ({ limit, offset }) => {
162
- const limitValue = limit || 10;
163
- const offsetValue = offset || 0;
164
- const data = await apiRequest(`/api/v1/mcp/test-runs?limit=${limitValue}&offset=${offsetValue}`);
165
- return {
166
- content: [
167
- {
168
- type: 'text',
169
- text: JSON.stringify(data, null, 2),
170
- },
171
- ],
172
- };
173
- }));
174
- }
175
- // Register tool: Generate test plan
176
- if (canRegisterTool('generate_test_plan')) {
177
- server.registerTool('generate_test_plan', {
178
- description: 'Create a draft test and trigger TestDetailsWriter to generate detailed test steps',
179
- inputSchema: {
180
- name: z.string().describe('Test name/title'),
181
- description: z.string().optional().describe('Test description'),
182
- targetUrl: z
183
- .string()
184
- .optional()
185
- .describe('Target URL for the test'),
186
- additionalInstructions: z
187
- .string()
188
- .optional()
189
- .describe('Additional instructions for TestDetailsWriter'),
190
- priority: z
191
- .enum(['p0', 'p1', 'p2', 'p3'])
192
- .optional()
193
- .describe('Test priority (default: p2)'),
194
- },
195
- }, wrapWithLogging('generate_test_plan', async ({ name, description, targetUrl, additionalInstructions, priority, }) => {
196
- // Step 1: Create draft test
197
- const createTestBody = {
198
- test: {
199
- name,
200
- description,
201
- priority: priority || 'p2',
202
- },
203
- };
204
- const createTestResponse = await apiRequest('/api/v1/mcp/tests', {
205
- method: 'POST',
206
- body: JSON.stringify(createTestBody),
207
- });
208
- const testId = createTestResponse.test.id;
209
- // Step 2: Trigger TestDetailsWriter
210
- const generateDetailsBody = {
211
- eventType: 'GenerateTestDetails',
212
- payload: {
213
- organizationId: orgId,
214
- testId,
215
- targetUrl,
216
- additionalInstructions,
217
- authenticationNeeded: true,
218
- updateStatusOnSuccess: true,
219
- },
220
- };
221
- const generateResponse = await apiRequest('/api/v1/mcp/agent-jobs', {
222
- method: 'POST',
223
- body: JSON.stringify(generateDetailsBody),
224
- });
225
- return {
226
- content: [
227
- {
228
- type: 'text',
229
- text: `Test plan generation initiated:\n\nTest Created:\n- ID: ${testId}\n- Name: ${name}\n- Organization: ${orgId}\n\nTestDetailsWriter Job:\n- Event ID: ${generateResponse.eventId}\n\nThe AI agent will now generate detailed test steps. You can check the test status using the test ID: ${testId}`,
230
- },
231
- ],
232
- };
233
- }));
234
- }
235
- // Register tool: Get active tests
236
- if (canRegisterTool('get_active_tests')) {
237
- server.registerTool('get_active_tests', {
238
- description: 'Get active tests for the organization. Returns a summary of each test (id, name, status, priority). Use get_test_details for full test information.',
239
- inputSchema: {
240
- limit: z
241
- .number()
242
- .optional()
243
- .describe('Maximum number of tests to return (default: 20, max: 50)'),
244
- offset: z
245
- .number()
246
- .optional()
247
- .describe('Pagination offset (default: 0)'),
248
- },
249
- }, wrapWithLogging('get_active_tests', async ({ limit, offset }) => {
250
- const limitValue = Math.min(limit || 20, 50); // Cap at 50
251
- const offsetValue = offset || 0;
252
- const testStatuses = encodeURIComponent(JSON.stringify(['active']));
253
- const data = await apiRequest(`/api/v1/mcp/tests?testStatuses=${testStatuses}&limit=${limitValue}&offset=${offsetValue}`);
254
- // Return summarized test data to reduce response size
255
- const summarizedTests = (data.items || []).map((test) => ({
256
- id: test.id,
257
- name: test.name,
258
- status: test.status,
259
- priority: test.priority,
260
- description: test.description?.substring(0, 200) || null,
261
- lastUpdated: test.lastUpdated,
262
- }));
263
- const total = data.totalCount || data.total || summarizedTests.length;
264
- return {
265
- content: [
266
- {
267
- type: 'text',
268
- text: JSON.stringify({
269
- tests: summarizedTests,
270
- total,
271
- limit: limitValue,
272
- offset: offsetValue,
273
- hasMore: total > offsetValue + limitValue,
274
- }, null, 2),
275
- },
276
- ],
277
- };
278
- }));
279
- }
280
- // Register tool: Update test status
281
- if (canRegisterTool('update_test_status')) {
282
- server.registerTool('update_test_status', {
283
- description: 'Bulk update the status of multiple tests',
284
- inputSchema: {
285
- testIds: z
286
- .array(z.string())
287
- .describe('Array of test IDs to update (e.g., ["test_xxx", "test_yyy"])'),
288
- status: z
289
- .enum([
290
- 'active',
291
- 'blocked_by_customer',
292
- 'under_maintenance',
293
- 'ignored',
294
- 'draft',
295
- 'ready_for_review',
296
- 'requested',
297
- 'expected_failure',
298
- 'canceled',
299
- 'suggested',
300
- ])
301
- .describe('The new status to set for all tests'),
302
- reason: z.string().describe('Reason for the status change'),
303
- category: z
304
- .string()
305
- .optional()
306
- .describe('Optional category for the status change'),
307
- updatedBy: z
308
- .string()
309
- .default('mcp-tool')
310
- .describe('Who is updating the status (default: mcp-tool)'),
311
- triggerMaintenanceJobs: z
312
- .boolean()
313
- .optional()
314
- .describe('Whether to trigger maintenance jobs (default: false)'),
315
- triggerCodegenJobs: z
316
- .boolean()
317
- .optional()
318
- .describe('Whether to trigger codegen jobs (default: false)'),
319
- },
320
- }, wrapWithLogging('update_test_status', async ({ testIds, status, reason, category, updatedBy, triggerMaintenanceJobs, triggerCodegenJobs, }) => {
321
- const requestBody = {
322
- bulkUpdateType: 'status',
323
- ids: testIds,
324
- change: {
325
- status,
326
- reason,
327
- category,
328
- updatedBy: updatedBy || 'mcp-tool',
329
- },
330
- triggerMaintenanceJobs,
331
- triggerCodegenJobs,
332
- };
333
- const data = await apiRequest('/api/v1/mcp/tests/bulk-update', {
334
- method: 'PATCH',
335
- body: JSON.stringify(requestBody),
336
- });
337
- return {
338
- content: [
339
- {
340
- type: 'text',
341
- text: `Successfully updated ${data.updatedCount} test(s) to status "${status}".\n\nDetails:\n${JSON.stringify(data, null, 2)}`,
342
- },
343
- ],
344
- };
345
- }));
346
- }
347
- // Register tool: Skip tests from description
348
- if (canRegisterTool('skip_tests_from_description')) {
349
- server.registerTool('skip_tests_from_description', {
350
- description: 'Skip tests based on a user description by changing their status to under_maintenance',
351
- inputSchema: {
352
- userRequest: z
353
- .string()
354
- .describe('The user request describing which tests to skip (e.g., "skip all login tests")'),
355
- },
356
- }, wrapWithLogging('skip_tests_from_description', async ({ userRequest }) => {
357
- return {
358
- content: [
359
- {
360
- type: 'text',
361
- text: `To skip tests based on the request: "${userRequest}"
362
-
363
- Follow these steps:
364
-
365
- 1. Run get_active_tests to retrieve all active tests for the organization
366
- 2. Review the test names and descriptions in the results
367
- 3. Identify which test IDs are relevant to the user's request: "${userRequest}"
368
- 4. Call update_test_status with:
369
- - testIds: array of relevant test IDs you identified
370
- - status: "under_maintenance"
371
- - reason: "${userRequest}"
372
- - updatedBy: the user's name or "mcp-tool"
373
-
374
- This will change the selected tests from "active" to "under_maintenance" status, effectively skipping them.`,
375
- },
376
- ],
377
- };
378
- }));
379
- }
380
- } // end registerTools
381
- // Factory function to create a new MCP server instance
382
- async function createMcpServer(token) {
383
- const mcpServer = new McpServer({
384
- name: 'ranger',
385
- version: '1.0.0',
386
- });
387
- // Register all tools on this server instance with the token
388
- await registerTools(mcpServer, token);
389
- return mcpServer;
390
- }
391
- // Start the server
392
- async function main() {
393
- const httpServer = createServer(async (req, res) => {
394
- // Enable CORS
395
- res.setHeader('Access-Control-Allow-Origin', '*');
396
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
397
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
398
- if (req.method === 'OPTIONS') {
399
- res.writeHead(204);
400
- res.end();
401
- return;
402
- }
403
- const url = new URL(req.url || '/', `http://localhost:${PORT}`);
404
- // Extract token from Authorization header (supports both "Bearer <token>" and raw token)
405
- const authHeader = req.headers.authorization;
406
- const token = authHeader?.startsWith('Bearer ')
407
- ? authHeader.slice(7)
408
- : authHeader || undefined;
409
- console.error(`[MCP] ${req.method} ${url.pathname} - Auth header: ${authHeader ? 'present' : 'missing'}, Token: ${token ? token.substring(0, 10) + '...' : 'none'}`);
410
- if (url.pathname === '/health') {
411
- res.writeHead(200, { 'Content-Type': 'application/json' });
412
- res.end(JSON.stringify({ status: 'ok' }));
413
- }
414
- else {
415
- // Route all other paths to MCP handler
416
- // Stateless mode - create a new server and transport for each request
417
- // This allows horizontal scaling across multiple Cloud Run instances
418
- const transport = new StreamableHTTPServerTransport({
419
- sessionIdGenerator: undefined, // Stateless mode
420
- });
421
- // Create server with token bound to all API calls
422
- const mcpServer = await createMcpServer(token);
423
- await mcpServer.connect(transport);
424
- await transport.handleRequest(req, res);
425
- }
426
- });
427
- httpServer.listen(PORT, () => {
428
- console.error(`Ranger MCP server running on http://localhost:${PORT}`);
429
- console.error(` MCP endpoint: http://localhost:${PORT}/mcp`);
430
- console.error(` Health check: http://localhost:${PORT}/health`);
431
- });
432
- }
433
- main().catch((error) => {
434
- console.error('Server error:', error);
435
- process.exit(1);
436
- });
@@ -1,13 +0,0 @@
1
- import { chromium } from 'playwright';
2
- async function main() {
3
- const browser = await chromium.launch({ headless: false });
4
- const context = await browser.newContext({
5
- storageState: '/Users/adwithmukherjee/dev/lavender-core/lavender-core/.ranger/debug-tool/local/auth.json',
6
- });
7
- const page = await context.newPage();
8
- await page.goto('http://localhost:3000');
9
- // Keep browser open
10
- console.log('Browser opened. Press Ctrl+C to close.');
11
- await new Promise(() => { });
12
- }
13
- main().catch(console.error);