kiroo 0.8.0 → 0.9.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/bin/kiroo.js CHANGED
@@ -1,288 +1,361 @@
1
- #!/usr/bin/env node
2
-
3
- import { Command } from 'commander';
4
- import chalk from 'chalk';
5
- import { executeRequest } from '../src/executor.js';
6
- import { listInteractions, replayInteraction } from '../src/replay.js';
7
- import { saveSnapshot, compareSnapshots, listSnapshots } from '../src/snapshot.js';
8
- import { setEnv, setVar, deleteVar, listEnv } from '../src/env.js';
9
- import { showGraph } from '../src/graph.js';
10
- import { validateResponse, showCheckResult } from '../src/checker.js';
11
- import { initProject } from '../src/init.js';
12
- import { showStats } from '../src/stats.js';
13
- import { handleImport } from '../src/import.js';
14
- import { clearAllInteractions } from '../src/storage.js';
15
- import { runBenchmark } from '../src/bench.js';
16
- import { editInteraction } from '../src/edit.js';
17
- import { exportToPostman } from '../src/export.js';
18
-
19
- const program = new Command();
20
-
21
- program
22
- .name('kiroo')
23
- .description('Git for API interactions. Record, replay, snapshot, and diff your APIs.')
24
- .version('0.8.0')
25
- .option('--lang <language>', 'Translate output to specified language (e.g., hi, es, fr)');
26
-
27
- // Init command
28
- program
29
- .command('init')
30
- .description('Initialize Kiroo in current directory')
31
- .action(async () => {
32
- await initProject();
33
- });
34
-
35
- // Check command (Zero-Code Testing)
36
- program
37
- .command('check <url>')
38
- .description('Execute a request and validate the response against rules')
39
- .option('-m, --method <method>', 'HTTP method (GET, POST, etc.)', 'GET')
40
- .option('-H, --header <header...>', 'Add custom headers')
41
- .option('-d, --data <data>', 'Request body (JSON or shorthand)')
42
- .option('--status <code...>', 'Expected HTTP status code')
43
- .option('--has <fields...>', 'Comma-separated list of expected fields in JSON response')
44
- .option('--match <matches...>', 'Expected field values (e.g., status=active)')
45
- .action(async (url, options) => {
46
- // Execute request
47
- const response = await executeRequest(options.method || 'GET', url, {
48
- header: options.header,
49
- data: options.data,
50
- });
51
-
52
- if (!response) {
53
- console.error(chalk.red('\n ✗ No response received to validate.'));
54
- process.exit(1);
55
- }
56
-
57
- // Parse matches: ["key1=val1", "key2=val2"] -> { key1: val1, key2: val2 }
58
- const matchObj = {};
59
- if (options.match) {
60
- options.match.forEach(m => {
61
- const [k, ...v] = m.split('=');
62
- if (k) matchObj[k] = v.join('=');
63
- });
64
- }
65
-
66
- // Parse has: ["id,name"] or ["id", "name"] -> ["id", "name"]
67
- const hasFields = options.has ? options.has.flatMap(h => h.split(',')).map(f => f.trim()) : [];
68
-
69
- // Construct rules
70
- const rules = {
71
- status: Array.isArray(options.status) ? options.status[0] : options.status,
72
- has: hasFields,
73
- match: matchObj
74
- };
75
-
76
- // Validate
77
- const validation = validateResponse(response, rules);
78
- showCheckResult(validation);
79
-
80
- if (!validation.passed) {
81
- process.exit(1);
82
- }
83
- });
84
- // sk_live_p7BWJjsYlKmauBOjiEeiLRuu4DokkBWsgYne_E6osTo
85
-
86
- // HTTP methods as commands
87
- ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'].forEach(method => {
88
- program
89
- .command(`${method.toLowerCase()} <url>`)
90
- .alias(method)
91
- .description(`Execute ${method} request and store interaction`)
92
- .option('-H, --header <headers...>', 'Headers (key:value)')
93
- .option('-d, --data <data>', 'Request body (JSON string or key=value pairs)')
94
- .option('-s, --save <pairs...>', 'Extract values from response to env (key=path.to.data)')
95
- .action(async (url, options) => {
96
- await executeRequest(method, url, options);
97
- });
98
- });
99
-
100
- // List interactions
101
- program
102
- .command('list')
103
- .description('List all stored interactions')
104
- .option('-n, --limit <number>', 'Number of interactions to show', '10')
105
- .option('-o, --offset <number>', 'Offset for pagination', '0')
106
- .option('--date <date>', 'Filter by date (YYYY-MM-DD)')
107
- .option('--url <url>', 'Filter by URL path')
108
- .option('--status <status>', 'Filter by HTTP status')
109
- .action(async (options) => {
110
- await listInteractions(options);
111
- });
112
-
113
- // Replay interaction
114
- program
115
- .command('replay <id>')
116
- .description('Replay a stored interaction')
117
- .action(async (id) => {
118
- await replayInteraction(id);
119
- });
120
-
121
- // Edit interaction
122
- program
123
- .command('edit <id>')
124
- .description('Edit an interaction in your text editor and quickly replay it')
125
- .action(async (id) => {
126
- await editInteraction(id);
127
- });
128
-
129
- // Export interactions
130
- program
131
- .command('export')
132
- .description('Export all stored interactions to a Postman Collection')
133
- .option('-o, --out <filename>', 'Output JSON filename', 'kiroo-collection.json')
134
- .action((options) => {
135
- exportToPostman(options.out);
136
- });
137
-
138
- // Bench command (Load Testing)
139
- program
140
- .command('bench <url>')
141
- .description('Run a basic load test against an endpoint')
142
- .option('-m, --method <method>', 'HTTP method (GET, POST, etc.)', 'GET')
143
- .option('-n, --number <number>', 'Number of total requests to send', '10')
144
- .option('-c, --concurrent <number>', 'Number of concurrent requests', '1')
145
- .option('-H, --header <header...>', 'Add custom headers')
146
- .option('-v, --verbose', 'Show detailed output for every request')
147
- .option('-d, --data <data>', 'Request body')
148
- .action(async (url, options) => {
149
- await runBenchmark(url, options);
150
- });
151
-
152
- // Clear command
153
- program
154
- .command('clear')
155
- .description('Clear all stored interaction history')
156
- .option('-f, --force', 'Force clear without confirmation')
157
- .action(async (options) => {
158
- if (!options.force) {
159
- const inquirer = (await import('inquirer')).default;
160
- const { confirm } = await inquirer.prompt([
161
- {
162
- type: 'confirm',
163
- name: 'confirm',
164
- message: chalk.red('Are you sure you want to clear all history?'),
165
- default: false
166
- }
167
- ]);
168
- if (!confirm) return;
169
- }
170
- clearAllInteractions();
171
- console.log(chalk.green('\n ✨ History cleared successfully.\n'));
172
- });
173
-
174
- // Environment commands
175
- const env = program.command('env').description('Environment management');
176
-
177
- env
178
- .command('use <name>')
179
- .description('Switch to a specific environment')
180
- .action((name) => setEnv(name));
181
-
182
- env
183
- .command('list')
184
- .description('List environments and variables')
185
- .action(() => listEnv());
186
-
187
- env
188
- .command('set <key> <value>')
189
- .description('Set a variable in current environment')
190
- .action((key, value) => setVar(key, value));
191
-
192
- env
193
- .command('rm <key>')
194
- .description('Remove a variable from current environment')
195
- .action((key) => deleteVar(key));
196
-
197
- // Snapshot commands
198
- const snapshot = program.command('snapshot').description('Snapshot management');
199
-
200
- snapshot
201
- .command('save <tag>')
202
- .description('Save current state as snapshot')
203
- .action(async (tag) => {
204
- await saveSnapshot(tag);
205
- });
206
-
207
- snapshot
208
- .command('list')
209
- .description('List all snapshots')
210
- .action(async () => {
211
- await listSnapshots();
212
- });
213
-
214
- snapshot
215
- .command('compare <tag1> <tag2>')
216
- .description('Compare two snapshots')
217
- .action(async (tag1, tag2) => {
218
- const opts = program.opts();
219
- await compareSnapshots(tag1, tag2, opts.lang);
220
- });
221
-
222
- // Graph command
223
- program
224
- .command('graph')
225
- .description('Show visual dependency graph of API interactions')
226
- .action(async () => {
227
- await showGraph();
228
- });
229
-
230
- // Import command
231
- program
232
- .command('import')
233
- .description('Import a request from a cURL command (opens editor if no command is provided)')
234
- .allowUnknownOption()
235
- .action(async (_, command) => {
236
- if (command.args.length > 0) {
237
- // Pass tokens directly to handleImport
238
- await handleImport(command.args);
239
- } else {
240
- const inquirer = (await import('inquirer')).default;
241
- const response = await inquirer.prompt([
242
- {
243
- type: 'editor',
244
- name: 'curl',
245
- message: 'Paste your cURL command here (opens your default editor):',
246
- validate: (input) => input.trim().length > 0 || 'Please enter a cURL command'
247
- }
248
- ]);
249
- await handleImport(response.curl);
250
- }
251
- });
252
-
253
- // Graph command
254
- /*
255
- program
256
- .command('graph')
257
- .description('Show API dependency graph')
258
- .action(async () => {
259
- await showGraph();
260
- });
261
- */
262
-
263
- // Stats command
264
- program
265
- .command('stats')
266
- .description('Show usage statistics')
267
- .action(async () => {
268
- await showStats();
269
- });
270
-
271
- // Error handling
272
- program.exitOverride();
273
-
274
- try {
275
- await program.parseAsync(process.argv);
276
- } catch (err) {
277
- if (err.code === 'commander.help' || err.message === '(outputHelp)') {
278
- // Help was requested, exit normally
279
- process.exit(0);
280
- } else if (err.code === 'commander.unknownCommand') {
281
- console.error(chalk.red(`\n ✗ Unknown command: ${err.message}\n`));
282
- console.log(chalk.gray(' Run'), chalk.white('kiroo --help'), chalk.gray('for usage information.\n'));
283
- process.exit(1);
284
- } else {
285
- console.error(chalk.red('\n ✗ Error:'), err.message, `(${err.code})`, '\n');
286
- process.exit(1);
287
- }
288
- }
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import chalk from 'chalk';
5
+ import { executeRequest } from '../src/executor.js';
6
+ import { listInteractions, replayInteraction } from '../src/replay.js';
7
+ import { saveSnapshot, compareSnapshots, listSnapshots } from '../src/snapshot.js';
8
+ import { setEnv, setVar, deleteVar, listEnv } from '../src/env.js';
9
+ import { showGraph } from '../src/graph.js';
10
+ import { validateResponse, showCheckResult } from '../src/checker.js';
11
+ import { initProject } from '../src/init.js';
12
+ import { showStats } from '../src/stats.js';
13
+ import { handleImport } from '../src/import.js';
14
+ import { clearAllInteractions, scrubStoredData } from '../src/storage.js';
15
+ import { runBenchmark } from '../src/bench.js';
16
+ import { editInteraction } from '../src/edit.js';
17
+ import { exportInteractions } from '../src/export.js';
18
+ import { runProxy } from '../src/proxy.js';
19
+ import { analyzeSnapshots } from '../src/analyze.js';
20
+
21
+ const program = new Command();
22
+
23
+ program
24
+ .name('kiroo')
25
+ .description('Git for API interactions. Record, replay, snapshot, and diff your APIs.')
26
+ .version('0.8.0')
27
+ .option('--lang <language>', 'Translate output to specified language (e.g., hi, es, fr)');
28
+
29
+ // Init command
30
+ program
31
+ .command('init')
32
+ .description('Initialize Kiroo in current directory')
33
+ .action(async () => {
34
+ await initProject();
35
+ });
36
+
37
+
38
+ // Check command (Zero-Code Testing)
39
+ program
40
+ .command('check <url>')
41
+ .description('Execute a request and validate the response against rules')
42
+ .option('-m, --method <method>', 'HTTP method (GET, POST, etc.)', 'GET')
43
+ .option('-H, --header <header...>', 'Add custom headers')
44
+ .option('-d, --data <data>', 'Request body (JSON or shorthand)')
45
+ .option('--status <code...>', 'Expected HTTP status code')
46
+ .option('--has <fields...>', 'Comma-separated list of expected fields in JSON response')
47
+ .option('--match <matches...>', 'Expected field values (e.g., status=active)')
48
+ .action(async (url, options) => {
49
+ // Execute request
50
+ const response = await executeRequest(options.method || 'GET', url, {
51
+ header: options.header,
52
+ data: options.data,
53
+ });
54
+
55
+ if (!response) {
56
+ console.error(chalk.red('\n ✗ No response received to validate.'));
57
+ process.exit(1);
58
+ }
59
+
60
+ // Parse matches: ["key1=val1", "key2=val2"] -> { key1: val1, key2: val2 }
61
+ const matchObj = {};
62
+ if (options.match) {
63
+ options.match.forEach(m => {
64
+ const [k, ...v] = m.split('=');
65
+ if (k) matchObj[k] = v.join('=');
66
+ });
67
+ }
68
+
69
+ // Parse has: ["id,name"] or ["id", "name"] -> ["id", "name"]
70
+ const hasFields = options.has ? options.has.flatMap(h => h.split(',')).map(f => f.trim()) : [];
71
+
72
+ // Construct rules
73
+ const rules = {
74
+ status: Array.isArray(options.status) ? options.status[0] : options.status,
75
+ has: hasFields,
76
+ match: matchObj
77
+ };
78
+
79
+ // Validate
80
+ const validation = validateResponse(response, rules);
81
+ showCheckResult(validation);
82
+
83
+ if (!validation.passed) {
84
+ process.exit(1);
85
+ }
86
+ });
87
+ // sk_live_p7BWJjsYlKmauBOjiEeiLRuu4DokkBWsgYne_E6osTo
88
+
89
+ // HTTP methods as commands
90
+ ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'].forEach(method => {
91
+ program
92
+ .command(`${method.toLowerCase()} <url>`)
93
+ .alias(method)
94
+ .description(`Execute ${method} request and store interaction`)
95
+ .option('-H, --header <headers...>', 'Headers (key:value)')
96
+ .option('-d, --data <data>', 'Request body (JSON string or key=value pairs)')
97
+ .option('-s, --save <pairs...>', 'Extract values from response to env (key=path.to.data)')
98
+ .action(async (url, options) => {
99
+ await executeRequest(method, url, options);
100
+ });
101
+ });
102
+
103
+ // List interactions
104
+ program
105
+ .command('list')
106
+ .description('List all stored interactions')
107
+ .option('-n, --limit <number>', 'Number of interactions to show', '10')
108
+ .option('-o, --offset <number>', 'Offset for pagination', '0')
109
+ .option('--date <date>', 'Filter by date (YYYY-MM-DD)')
110
+ .option('--url <url>', 'Filter by URL path')
111
+ .option('--status <status>', 'Filter by HTTP status')
112
+ .action(async (options) => {
113
+ await listInteractions(options);
114
+ });
115
+
116
+ // Replay interaction
117
+ program
118
+ .command('replay <id>')
119
+ .description('Replay a stored interaction')
120
+ .action(async (id) => {
121
+ await replayInteraction(id);
122
+ });
123
+
124
+ // Edit interaction
125
+ program
126
+ .command('edit <id>')
127
+ .description('Edit an interaction in your text editor and quickly replay it')
128
+ .action(async (id) => {
129
+ await editInteraction(id);
130
+ });
131
+
132
+ // Export interactions
133
+ program
134
+ .command('export')
135
+ .description('Export stored interactions to Postman or OpenAPI')
136
+ .option('-f, --format <format>', 'Export format: postman|openapi', 'postman')
137
+ .option('-o, --out <filename>', 'Output JSON filename')
138
+ .option('--title <title>', 'OpenAPI title (openapi format only)')
139
+ .option('--api-version <version>', 'OpenAPI version (openapi format only)')
140
+ .option('--server <url>', 'OpenAPI server URL override')
141
+ .option('--path-prefix <prefix>', 'Only include endpoints that start with this path')
142
+ .option('--min-samples <number>', 'Only include operations seen at least N times')
143
+ .action((options) => {
144
+ exportInteractions(options);
145
+ });
146
+
147
+ // Bench command (Load Testing)
148
+ program
149
+ .command('bench <url>')
150
+ .description('Run a basic load test against an endpoint')
151
+ .option('-m, --method <method>', 'HTTP method (GET, POST, etc.)', 'GET')
152
+ .option('-n, --number <number>', 'Number of total requests to send', '10')
153
+ .option('-c, --concurrent <number>', 'Number of concurrent requests', '1')
154
+ .option('-H, --header <header...>', 'Add custom headers')
155
+ .option('-v, --verbose', 'Show detailed output for every request')
156
+ .option('-d, --data <data>', 'Request body')
157
+ .action(async (url, options) => {
158
+ await runBenchmark(url, options);
159
+ });
160
+
161
+ // Clear command
162
+ program
163
+ .command('clear')
164
+ .description('Clear all stored interaction history')
165
+ .option('-f, --force', 'Force clear without confirmation')
166
+ .action(async (options) => {
167
+ if (!options.force) {
168
+ const inquirer = (await import('inquirer')).default;
169
+ const { confirm } = await inquirer.prompt([
170
+ {
171
+ type: 'confirm',
172
+ name: 'confirm',
173
+ message: chalk.red('Are you sure you want to clear all history?'),
174
+ default: false
175
+ }
176
+ ]);
177
+ if (!confirm) return;
178
+ }
179
+ clearAllInteractions();
180
+ console.log(chalk.green('\n ✨ History cleared successfully.\n'));
181
+ });
182
+
183
+ program
184
+ .command('scrub')
185
+ .description('Redact sensitive data in stored interactions and snapshots')
186
+ .option('--dry-run', 'Show what would be changed without modifying files')
187
+ .action((options) => {
188
+ const summary = scrubStoredData({ dryRun: !!options.dryRun });
189
+
190
+ console.log(chalk.cyan('\n 🧼 Scrub summary'));
191
+ console.log(chalk.gray(` Interactions: ${summary.interactions.updated}/${summary.interactions.scanned} updated`));
192
+ console.log(chalk.gray(` Snapshots: ${summary.snapshots.updated}/${summary.snapshots.scanned} updated`));
193
+
194
+ if (summary.totalUpdated === 0) {
195
+ console.log(chalk.green('\n ✅ No sensitive changes needed.\n'));
196
+ return;
197
+ }
198
+
199
+ if (summary.dryRun) {
200
+ console.log(chalk.yellow('\n ⚠️ Dry run only. Re-run without --dry-run to apply changes.\n'));
201
+ return;
202
+ }
203
+
204
+ console.log(chalk.green('\n ✅ Sensitive fields were redacted successfully.\n'));
205
+ });
206
+
207
+ // Environment commands
208
+ const env = program.command('env').description('Environment management');
209
+
210
+ env
211
+ .command('use <name>')
212
+ .description('Switch to a specific environment')
213
+ .action((name) => setEnv(name));
214
+
215
+ env
216
+ .command('list')
217
+ .description('List environments and variables')
218
+ .action(() => listEnv());
219
+
220
+ env
221
+ .command('set <key> <value>')
222
+ .description('Set a variable in current environment')
223
+ .action((key, value) => setVar(key, value));
224
+
225
+ env
226
+ .command('rm <key>')
227
+ .description('Remove a variable from current environment')
228
+ .action((key) => deleteVar(key));
229
+
230
+ // Snapshot commands
231
+ const snapshot = program.command('snapshot').description('Snapshot management');
232
+
233
+ snapshot
234
+ .command('save <tag>')
235
+ .description('Save current state as snapshot')
236
+ .action(async (tag) => {
237
+ await saveSnapshot(tag);
238
+ });
239
+
240
+ snapshot
241
+ .command('list')
242
+ .description('List all snapshots')
243
+ .action(async () => {
244
+ await listSnapshots();
245
+ });
246
+
247
+ snapshot
248
+ .command('compare <tag1> <tag2>')
249
+ .description('Compare two snapshots')
250
+ .option('--analyze', 'Run semantic analysis right after structural compare')
251
+ .option('--ai', 'When used with --analyze, include AI summary')
252
+ .option('--model <model>', 'Model override for analyze --ai mode')
253
+ .option('--max-tokens <number>', 'Max completion tokens for analyze --ai mode')
254
+ .option('--fail-on <severity>', 'Severity threshold for analyze mode (low|medium|high|critical)')
255
+ .action(async (tag1, tag2, options) => {
256
+ const opts = program.opts();
257
+ await compareSnapshots(tag1, tag2, opts.lang);
258
+ if (options.analyze) {
259
+ await analyzeSnapshots(tag1, tag2, {
260
+ ai: !!options.ai,
261
+ model: options.model,
262
+ maxTokens: options.maxTokens,
263
+ failOn: options.failOn,
264
+ lang: opts.lang,
265
+ });
266
+ }
267
+ });
268
+
269
+ program
270
+ .command('analyze <tag1> <tag2>')
271
+ .description('Semantic blast-radius analysis between two snapshots')
272
+ .option('--json', 'Output structured JSON report')
273
+ .option('--ai', 'Generate Groq-powered impact summary (uses GROQ_API_KEY)')
274
+ .option('--model <model>', 'Groq model override')
275
+ .option('--max-tokens <number>', 'Max completion tokens for AI summary')
276
+ .option('--fail-on <severity>', 'Exit non-zero when severity >= threshold (low|medium|high|critical)')
277
+ .action(async (tag1, tag2, options) => {
278
+ const opts = program.opts();
279
+ await analyzeSnapshots(tag1, tag2, {
280
+ ...options,
281
+ lang: opts.lang
282
+ });
283
+ });
284
+
285
+ // Graph command
286
+ program
287
+ .command('graph')
288
+ .description('Show visual dependency graph of API interactions')
289
+ .action(async () => {
290
+ await showGraph();
291
+ });
292
+
293
+ // Import command
294
+ program
295
+ .command('import')
296
+ .description('Import a request from a cURL command (opens editor if no command is provided)')
297
+ .allowUnknownOption()
298
+ .action(async (_, command) => {
299
+ if (command.args.length > 0) {
300
+ // Pass tokens directly to handleImport
301
+ await handleImport(command.args);
302
+ } else {
303
+ const inquirer = (await import('inquirer')).default;
304
+ const response = await inquirer.prompt([
305
+ {
306
+ type: 'editor',
307
+ name: 'curl',
308
+ message: 'Paste your cURL command here (opens your default editor):',
309
+ validate: (input) => input.trim().length > 0 || 'Please enter a cURL command'
310
+ }
311
+ ]);
312
+ await handleImport(response.curl);
313
+ }
314
+ });
315
+
316
+ // Graph command
317
+ /*
318
+ program
319
+ .command('graph')
320
+ .description('Show API dependency graph')
321
+ .action(async () => {
322
+ await showGraph();
323
+ });
324
+ */
325
+
326
+ // Proxy command
327
+ program
328
+ .command('proxy')
329
+ .description('Start a time-travel proxy to automatically record interactions')
330
+ .requiredOption('-t, --target <url>', 'Target URL to proxy requests to')
331
+ .option('-p, --port <port>', 'Port to listen on', '8080')
332
+ .action(async (options) => {
333
+ await runProxy(options.target, options);
334
+ });
335
+
336
+ // Stats command
337
+ program
338
+ .command('stats')
339
+ .description('Show usage statistics')
340
+ .action(async () => {
341
+ await showStats();
342
+ });
343
+
344
+ // Error handling
345
+ program.exitOverride();
346
+
347
+ try {
348
+ await program.parseAsync(process.argv);
349
+ } catch (err) {
350
+ if (err.code === 'commander.help' || err.message === '(outputHelp)') {
351
+ // Help was requested, exit normally
352
+ process.exit(0);
353
+ } else if (err.code === 'commander.unknownCommand') {
354
+ console.error(chalk.red(`\n ✗ Unknown command: ${err.message}\n`));
355
+ console.log(chalk.gray(' Run'), chalk.white('kiroo --help'), chalk.gray('for usage information.\n'));
356
+ process.exit(1);
357
+ } else {
358
+ console.error(chalk.red('\n ✗ Error:'), err.message, `(${err.code})`, '\n');
359
+ process.exit(1);
360
+ }
361
+ }