kiroo 0.2.0 → 0.3.4

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
@@ -8,14 +8,16 @@ import { saveSnapshot, compareSnapshots, listSnapshots } from '../src/snapshot.j
8
8
  import { setEnv, setVar, deleteVar, listEnv } from '../src/env.js';
9
9
  // import { showGraph } from '../src/graph.js';
10
10
  import { initProject } from '../src/init.js';
11
- // import { showStats } from '../src/stats.js';
11
+ import { showStats } from '../src/stats.js';
12
+ import { handleImport } from '../src/import.js';
13
+ import { clearAllInteractions } from '../src/storage.js';
12
14
 
13
15
  const program = new Command();
14
16
 
15
17
  program
16
18
  .name('kiroo')
17
19
  .description('Git for API interactions. Record, replay, snapshot, and diff your APIs.')
18
- .version('0.2.0');
20
+ .version('0.3.4');
19
21
 
20
22
  // Init command
21
23
  program
@@ -58,6 +60,28 @@ program
58
60
  await replayInteraction(id);
59
61
  });
60
62
 
63
+ // Clear command
64
+ program
65
+ .command('clear')
66
+ .description('Clear all stored interaction history')
67
+ .option('-f, --force', 'Force clear without confirmation')
68
+ .action(async (options) => {
69
+ if (!options.force) {
70
+ const inquirer = (await import('inquirer')).default;
71
+ const { confirm } = await inquirer.prompt([
72
+ {
73
+ type: 'confirm',
74
+ name: 'confirm',
75
+ message: chalk.red('Are you sure you want to clear all history?'),
76
+ default: false
77
+ }
78
+ ]);
79
+ if (!confirm) return;
80
+ }
81
+ clearAllInteractions();
82
+ console.log(chalk.green('\n ✨ History cleared successfully.\n'));
83
+ });
84
+
61
85
  // Environment commands
62
86
  const env = program.command('env').description('Environment management');
63
87
 
@@ -105,6 +129,29 @@ snapshot
105
129
  await compareSnapshots(tag1, tag2);
106
130
  });
107
131
 
132
+ // Import command
133
+ program
134
+ .command('import')
135
+ .description('Import a request from a cURL command (opens editor if no command is provided)')
136
+ .allowUnknownOption()
137
+ .action(async (_, command) => {
138
+ if (command.args.length > 0) {
139
+ // Pass tokens directly to handleImport
140
+ await handleImport(command.args);
141
+ } else {
142
+ const inquirer = (await import('inquirer')).default;
143
+ const response = await inquirer.prompt([
144
+ {
145
+ type: 'editor',
146
+ name: 'curl',
147
+ message: 'Paste your cURL command here (opens your default editor):',
148
+ validate: (input) => input.trim().length > 0 || 'Please enter a cURL command'
149
+ }
150
+ ]);
151
+ await handleImport(response.curl);
152
+ }
153
+ });
154
+
108
155
  // Graph command
109
156
  /*
110
157
  program
@@ -116,14 +163,12 @@ program
116
163
  */
117
164
 
118
165
  // Stats command
119
- /*
120
166
  program
121
167
  .command('stats')
122
168
  .description('Show usage statistics')
123
169
  .action(async () => {
124
170
  await showStats();
125
171
  });
126
- */
127
172
 
128
173
  // Error handling
129
174
  program.exitOverride();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kiroo",
3
- "version": "0.2.0",
3
+ "version": "0.3.4",
4
4
  "description": "Git for API interactions. Record, replay, snapshot, and diff your APIs.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/executor.js CHANGED
@@ -55,6 +55,48 @@ export async function executeRequest(method, url, options = {}) {
55
55
  // Apply replacements to URL
56
56
  url = applyEnvReplacements(url, currentEnvVars);
57
57
 
58
+ // Auto-BaseURL logic
59
+ if (currentEnvVars.baseUrl) {
60
+ let isRelative = false;
61
+ let pathPart = url;
62
+
63
+ // 1. Direct relative path
64
+ if (url.startsWith('/')) {
65
+ isRelative = true;
66
+ }
67
+ // 2. Windows Git Bash conversion: Detect "C:/..." style paths with no protocol
68
+ else if (process.platform === 'win32' && /^[a-zA-Z]:[/\\]/.test(url) && !url.includes('://')) {
69
+ isRelative = true;
70
+ // Extract the part after the drive letter and potential Git Bash root
71
+ // We look for common markers or just the first segment that looks like a path
72
+ // Most reliable for Git Bash: The user's path is at the end.
73
+ // We'll try to find the /api, /v1, etc., or just fallback to the full path after the first few segments
74
+ const segments = url.split(/[/\\]/);
75
+ const apiIdx = segments.findIndex(s => s === 'api' || s === 'v1' || s === 'v2');
76
+ if (apiIdx !== -1) {
77
+ pathPart = '/' + segments.slice(apiIdx).join('/');
78
+ } else {
79
+ // Fallback: If we can't find a marker, it's hard to guess safely,
80
+ // but we can try to strip the drive letter and first few segments
81
+ // In Git Bash, it's usually C:/Program Files/Git/api...
82
+ // Let's at least strip the drive letter root
83
+ pathPart = url.replace(/^[a-zA-Z]:/, '').replace(/\\/g, '/');
84
+ if (!pathPart.startsWith('/')) pathPart = '/' + pathPart;
85
+ }
86
+ }
87
+ // 3. No leading slash but doesn't look like a host (no dots, no protocol)
88
+ else if (!url.includes('://') && !url.includes('.') && !url.includes(':') && !url.startsWith('localhost')) {
89
+ isRelative = true;
90
+ pathPart = '/' + url;
91
+ }
92
+
93
+ if (isRelative) {
94
+ const baseUrl = currentEnvVars.baseUrl;
95
+ const normalizedBaseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
96
+ url = normalizedBaseUrl + (pathPart.startsWith('/') ? pathPart : '/' + pathPart);
97
+ }
98
+ }
99
+
58
100
  // Parse headers
59
101
  const headers = {};
60
102
  if (options.header) {
package/src/import.js ADDED
@@ -0,0 +1,113 @@
1
+ import chalk from 'chalk';
2
+ import { executeRequest } from './executor.js';
3
+
4
+ export async function handleImport(input) {
5
+ try {
6
+ let tokens = [];
7
+ if (Array.isArray(input)) {
8
+ tokens = input;
9
+ } else {
10
+ tokens = stringToTokens(input);
11
+ }
12
+
13
+ const parsed = parseCurlTokens(tokens);
14
+
15
+ console.log(chalk.cyan('\n 📥 Parsed cURL Command:'));
16
+ console.log(chalk.gray(` Method: `), chalk.white(parsed.method));
17
+ console.log(chalk.gray(` URL: `), chalk.white(parsed.url));
18
+ console.log(chalk.gray(` Headers:`), chalk.white(Object.keys(parsed.headers).length));
19
+ if (parsed.body) {
20
+ console.log(chalk.gray(` Body: `), chalk.white('Present'));
21
+ }
22
+ console.log('');
23
+
24
+ // Convert parsed object to options format for executeRequest
25
+ const options = {
26
+ header: Object.entries(parsed.headers).map(([k, v]) => `${k}: ${v}`),
27
+ data: parsed.body
28
+ };
29
+
30
+ await executeRequest(parsed.method, parsed.url, options);
31
+
32
+ } catch (error) {
33
+ console.error(chalk.red('\n ✗ Import failed:'), error.message, '\n');
34
+ }
35
+ }
36
+
37
+ function stringToTokens(curlString) {
38
+ // Clean up shell artifacts (Windows ^, backslashes for line continuation)
39
+ let cleanStr = curlString.replace(/\^/g, '').replace(/\\\n/g, ' ').trim();
40
+
41
+ const tokens = [];
42
+ let currentToken = '';
43
+ let inQuotes = false;
44
+ let quoteChar = '';
45
+
46
+ for (let i = 0; i < cleanStr.length; i++) {
47
+ const char = cleanStr[i];
48
+ if ((char === "'" || char === '"') && (i === 0 || cleanStr[i - 1] !== '\\')) {
49
+ if (inQuotes && char === quoteChar) {
50
+ inQuotes = false;
51
+ tokens.push(currentToken);
52
+ currentToken = '';
53
+ quoteChar = '';
54
+ } else if (!inQuotes) {
55
+ inQuotes = true;
56
+ quoteChar = char;
57
+ } else {
58
+ currentToken += char;
59
+ }
60
+ } else if (char === ' ' && !inQuotes) {
61
+ if (currentToken) {
62
+ tokens.push(currentToken);
63
+ currentToken = '';
64
+ }
65
+ } else {
66
+ currentToken += char;
67
+ }
68
+ }
69
+ if (currentToken) tokens.push(currentToken);
70
+ return tokens;
71
+ }
72
+
73
+ function parseCurlTokens(tokens) {
74
+ const result = {
75
+ method: 'GET',
76
+ url: '',
77
+ headers: {},
78
+ body: ''
79
+ };
80
+
81
+ for (let i = 0; i < tokens.length; i++) {
82
+ const token = tokens[i].trim();
83
+ if (!token) continue;
84
+
85
+ if (token === '-X' || token === '--request') {
86
+ result.method = tokens[++i].toUpperCase();
87
+ } else if (token === '-H' || token === '--header') {
88
+ const headerStr = tokens[++i];
89
+ if (!headerStr) continue;
90
+ const colonIdx = headerStr.indexOf(':');
91
+ if (colonIdx !== -1) {
92
+ const key = headerStr.substring(0, colonIdx).trim();
93
+ const value = headerStr.substring(colonIdx + 1).trim();
94
+ result.headers[key] = value;
95
+ }
96
+ } else if (token === '-d' || token === '--data' || token === '--data-raw' || token === '--data-binary') {
97
+ result.body = tokens[++i];
98
+ if (result.method === 'GET') result.method = 'POST';
99
+ } else if (token.includes('://') || (token.startsWith('localhost') || token.startsWith('127.0.0.1'))) {
100
+ if (!result.url) result.url = token;
101
+ } else if (token === 'curl' || token.startsWith('-')) {
102
+ continue;
103
+ } else {
104
+ if (!result.url) result.url = token;
105
+ }
106
+ }
107
+
108
+ if (result.url) {
109
+ result.url = result.url.replace(/^["']|["']$/g, '');
110
+ }
111
+
112
+ return result;
113
+ }
package/src/stats.js ADDED
@@ -0,0 +1,73 @@
1
+ import chalk from 'chalk';
2
+ import Table from 'cli-table3';
3
+ import { getAllInteractions } from './storage.js';
4
+
5
+ export function showStats() {
6
+ const interactions = getAllInteractions();
7
+
8
+ if (interactions.length === 0) {
9
+ console.log(chalk.yellow('\n No interactions found to analyze.'));
10
+ console.log(chalk.gray(' Run some requests first to see the magic! ✨\n'));
11
+ return;
12
+ }
13
+
14
+ console.log(chalk.cyan.bold('\n 📊 Kiroo Analytics Dashboard\n'));
15
+
16
+ // 1. General Metrics
17
+ const total = interactions.length;
18
+ const successes = interactions.filter(i => i.response.status >= 200 && i.response.status < 300).length;
19
+ const successRate = ((successes / total) * 100).toFixed(1);
20
+ const avgDuration = (interactions.reduce((acc, i) => acc + i.metadata.duration_ms, 0) / total).toFixed(0);
21
+
22
+ const generalTable = new Table();
23
+ generalTable.push(
24
+ { [chalk.white('Total Requests')]: chalk.cyan(total) },
25
+ { [chalk.white('Success Rate')]: successRate >= 80 ? chalk.green(successRate + '%') : chalk.yellow(successRate + '%') },
26
+ { [chalk.white('Avg. Duration')]: chalk.white(avgDuration + 'ms') }
27
+ );
28
+
29
+ console.log(chalk.white.bold(' ● General Performance'));
30
+ console.log(generalTable.toString());
31
+
32
+ // 2. Method Distribution
33
+ const methods = {};
34
+ interactions.forEach(i => {
35
+ methods[i.request.method] = (methods[i.request.method] || 0) + 1;
36
+ });
37
+
38
+ const methodTable = new Table({
39
+ head: ['Method', 'Count', 'Percentage'].map(h => chalk.cyan(h)),
40
+ colWidths: [15, 10, 15]
41
+ });
42
+
43
+ Object.entries(methods).forEach(([method, count]) => {
44
+ const percentage = ((count / total) * 100).toFixed(1) + '%';
45
+ methodTable.push([chalk.white(method), chalk.white(count), chalk.gray(percentage)]);
46
+ });
47
+
48
+ console.log(chalk.white.bold('\n ● Request Distribution'));
49
+ console.log(methodTable.toString());
50
+
51
+ // 3. Slowest Endpoints
52
+ const slowTable = new Table({
53
+ head: ['URL', 'Status', 'Duration'].map(h => chalk.cyan(h)),
54
+ colWidths: [45, 10, 15]
55
+ });
56
+
57
+ const sortedBySlowest = [...interactions].sort((a, b) => b.metadata.duration_ms - a.metadata.duration_ms);
58
+ const topSlow = sortedBySlowest.slice(0, 5);
59
+
60
+ topSlow.forEach(i => {
61
+ const url = i.request.url.length > 42 ? i.request.url.substring(0, 39) + '...' : i.request.url;
62
+ const statusColor = i.response.status < 400 ? chalk.green : chalk.red;
63
+ slowTable.push([
64
+ chalk.gray(url),
65
+ statusColor(i.response.status),
66
+ chalk.red(i.metadata.duration_ms + 'ms')
67
+ ]);
68
+ });
69
+
70
+ console.log(chalk.white.bold('\n ● Top 5 Slowest Endpoints (Bottlenecks)'));
71
+ console.log(slowTable.toString());
72
+ console.log('');
73
+ }
package/src/storage.js CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync } from 'fs';
1
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, rmSync } from 'fs';
2
2
  import { join } from 'path';
3
3
 
4
4
  const KIROO_DIR = '.kiroo';
@@ -112,6 +112,17 @@ export function getAllSnapshots() {
112
112
  .map(f => f.replace('.json', ''));
113
113
  }
114
114
 
115
+ export function clearAllInteractions() {
116
+ ensureKirooDir();
117
+ if (existsSync(INTERACTIONS_DIR)) {
118
+ const files = readdirSync(INTERACTIONS_DIR);
119
+ files.forEach(f => {
120
+ const filepath = join(INTERACTIONS_DIR, f);
121
+ rmSync(filepath, { force: true });
122
+ });
123
+ }
124
+ }
125
+
115
126
  export function loadEnv() {
116
127
  ensureKirooDir();
117
128
  const data = readFileSync(ENV_FILE, 'utf8');