kiroo 0.7.4 → 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,267 +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
-
17
- const program = new Command();
18
-
19
- program
20
- .name('kiroo')
21
- .description('Git for API interactions. Record, replay, snapshot, and diff your APIs.')
22
- .version('0.7.4');
23
-
24
- // Init command
25
- program
26
- .command('init')
27
- .description('Initialize Kiroo in current directory')
28
- .action(async () => {
29
- await initProject();
30
- });
31
-
32
- // Check command (Zero-Code Testing)
33
- program
34
- .command('check <url>')
35
- .description('Execute a request and validate the response against rules')
36
- .option('-m, --method <method>', 'HTTP method (GET, POST, etc.)', 'GET')
37
- .option('-H, --header <header...>', 'Add custom headers')
38
- .option('-d, --data <data>', 'Request body (JSON or shorthand)')
39
- .option('--status <code...>', 'Expected HTTP status code')
40
- .option('--has <fields...>', 'Comma-separated list of expected fields in JSON response')
41
- .option('--match <matches...>', 'Expected field values (e.g., status=active)')
42
- .action(async (url, options) => {
43
- // Execute request
44
- const response = await executeRequest(options.method || 'GET', url, {
45
- header: options.header,
46
- data: options.data,
47
- });
48
-
49
- if (!response) {
50
- console.error(chalk.red('\n ✗ No response received to validate.'));
51
- process.exit(1);
52
- }
53
-
54
- // Parse matches: ["key1=val1", "key2=val2"] -> { key1: val1, key2: val2 }
55
- const matchObj = {};
56
- if (options.match) {
57
- options.match.forEach(m => {
58
- const [k, ...v] = m.split('=');
59
- if (k) matchObj[k] = v.join('=');
60
- });
61
- }
62
-
63
- // Parse has: ["id,name"] or ["id", "name"] -> ["id", "name"]
64
- const hasFields = options.has ? options.has.flatMap(h => h.split(',')).map(f => f.trim()) : [];
65
-
66
- // Construct rules
67
- const rules = {
68
- status: Array.isArray(options.status) ? options.status[0] : options.status,
69
- has: hasFields,
70
- match: matchObj
71
- };
72
-
73
- // Validate
74
- const validation = validateResponse(response, rules);
75
- showCheckResult(validation);
76
-
77
- if (!validation.passed) {
78
- process.exit(1);
79
- }
80
- });
81
- // sk_live_p7BWJjsYlKmauBOjiEeiLRuu4DokkBWsgYne_E6osTo
82
-
83
- // HTTP methods as commands
84
- ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'].forEach(method => {
85
- program
86
- .command(`${method.toLowerCase()} <url>`)
87
- .alias(method)
88
- .description(`Execute ${method} request and store interaction`)
89
- .option('-H, --header <headers...>', 'Headers (key:value)')
90
- .option('-d, --data <data>', 'Request body (JSON string or key=value pairs)')
91
- .option('-s, --save <pairs...>', 'Extract values from response to env (key=path.to.data)')
92
- .action(async (url, options) => {
93
- await executeRequest(method, url, options);
94
- });
95
- });
96
-
97
- // List interactions
98
- program
99
- .command('list')
100
- .description('List all stored interactions')
101
- .option('-n, --limit <number>', 'Number of interactions to show', '10')
102
- .option('-o, --offset <number>', 'Offset for pagination', '0')
103
- .option('--date <date>', 'Filter by date (YYYY-MM-DD)')
104
- .option('--url <url>', 'Filter by URL path')
105
- .option('--status <status>', 'Filter by HTTP status')
106
- .action(async (options) => {
107
- await listInteractions(options);
108
- });
109
-
110
- // Replay interaction
111
- program
112
- .command('replay <id>')
113
- .description('Replay a stored interaction')
114
- .action(async (id) => {
115
- await replayInteraction(id);
116
- });
117
-
118
- // Bench command (Load Testing)
119
- program
120
- .command('bench <url>')
121
- .description('Run a basic load test against an endpoint')
122
- .option('-m, --method <method>', 'HTTP method (GET, POST, etc.)', 'GET')
123
- .option('-n, --number <number>', 'Number of total requests to send', '10')
124
- .option('-c, --concurrent <number>', 'Number of concurrent requests', '1')
125
- .option('-H, --header <header...>', 'Add custom headers')
126
- .option('-v, --verbose', 'Show detailed output for every request')
127
- .option('-d, --data <data>', 'Request body')
128
- .action(async (url, options) => {
129
- await runBenchmark(url, options);
130
- });
131
-
132
- // Clear command
133
- program
134
- .command('clear')
135
- .description('Clear all stored interaction history')
136
- .option('-f, --force', 'Force clear without confirmation')
137
- .action(async (options) => {
138
- if (!options.force) {
139
- const inquirer = (await import('inquirer')).default;
140
- const { confirm } = await inquirer.prompt([
141
- {
142
- type: 'confirm',
143
- name: 'confirm',
144
- message: chalk.red('Are you sure you want to clear all history?'),
145
- default: false
146
- }
147
- ]);
148
- if (!confirm) return;
149
- }
150
- clearAllInteractions();
151
- console.log(chalk.green('\n ✨ History cleared successfully.\n'));
152
- });
153
-
154
- // Environment commands
155
- const env = program.command('env').description('Environment management');
156
-
157
- env
158
- .command('use <name>')
159
- .description('Switch to a specific environment')
160
- .action((name) => setEnv(name));
161
-
162
- env
163
- .command('list')
164
- .description('List environments and variables')
165
- .action(() => listEnv());
166
-
167
- env
168
- .command('set <key> <value>')
169
- .description('Set a variable in current environment')
170
- .action((key, value) => setVar(key, value));
171
-
172
- env
173
- .command('rm <key>')
174
- .description('Remove a variable from current environment')
175
- .action((key) => deleteVar(key));
176
-
177
- // Snapshot commands
178
- const snapshot = program.command('snapshot').description('Snapshot management');
179
-
180
- snapshot
181
- .command('save <tag>')
182
- .description('Save current state as snapshot')
183
- .action(async (tag) => {
184
- await saveSnapshot(tag);
185
- });
186
-
187
- snapshot
188
- .command('list')
189
- .description('List all snapshots')
190
- .action(async () => {
191
- await listSnapshots();
192
- });
193
-
194
- snapshot
195
- .command('compare <tag1> <tag2>')
196
- .description('Compare two snapshots')
197
- .action(async (tag1, tag2) => {
198
- await compareSnapshots(tag1, tag2);
199
- });
200
-
201
- // Graph command
202
- program
203
- .command('graph')
204
- .description('Show visual dependency graph of API interactions')
205
- .action(async () => {
206
- await showGraph();
207
- });
208
-
209
- // Import command
210
- program
211
- .command('import')
212
- .description('Import a request from a cURL command (opens editor if no command is provided)')
213
- .allowUnknownOption()
214
- .action(async (_, command) => {
215
- if (command.args.length > 0) {
216
- // Pass tokens directly to handleImport
217
- await handleImport(command.args);
218
- } else {
219
- const inquirer = (await import('inquirer')).default;
220
- const response = await inquirer.prompt([
221
- {
222
- type: 'editor',
223
- name: 'curl',
224
- message: 'Paste your cURL command here (opens your default editor):',
225
- validate: (input) => input.trim().length > 0 || 'Please enter a cURL command'
226
- }
227
- ]);
228
- await handleImport(response.curl);
229
- }
230
- });
231
-
232
- // Graph command
233
- /*
234
- program
235
- .command('graph')
236
- .description('Show API dependency graph')
237
- .action(async () => {
238
- await showGraph();
239
- });
240
- */
241
-
242
- // Stats command
243
- program
244
- .command('stats')
245
- .description('Show usage statistics')
246
- .action(async () => {
247
- await showStats();
248
- });
249
-
250
- // Error handling
251
- program.exitOverride();
252
-
253
- try {
254
- await program.parseAsync(process.argv);
255
- } catch (err) {
256
- if (err.code === 'commander.help' || err.message === '(outputHelp)') {
257
- // Help was requested, exit normally
258
- process.exit(0);
259
- } else if (err.code === 'commander.unknownCommand') {
260
- console.error(chalk.red(`\n ✗ Unknown command: ${err.message}\n`));
261
- console.log(chalk.gray(' Run'), chalk.white('kiroo --help'), chalk.gray('for usage information.\n'));
262
- process.exit(1);
263
- } else {
264
- console.error(chalk.red('\n ✗ Error:'), err.message, `(${err.code})`, '\n');
265
- process.exit(1);
266
- }
267
- }
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
+ }