kiroo 0.2.0 → 0.4.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 +58 -5
- package/package.json +1 -1
- package/src/executor.js +58 -6
- package/src/graph.js +75 -0
- package/src/import.js +113 -0
- package/src/snapshot.js +16 -4
- package/src/stats.js +73 -0
- package/src/storage.js +14 -1
package/bin/kiroo.js
CHANGED
|
@@ -6,16 +6,18 @@ import { executeRequest } from '../src/executor.js';
|
|
|
6
6
|
import { listInteractions, replayInteraction } from '../src/replay.js';
|
|
7
7
|
import { saveSnapshot, compareSnapshots, listSnapshots } from '../src/snapshot.js';
|
|
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
|
-
|
|
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.
|
|
20
|
+
.version('0.4.0');
|
|
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,37 @@ snapshot
|
|
|
105
129
|
await compareSnapshots(tag1, tag2);
|
|
106
130
|
});
|
|
107
131
|
|
|
132
|
+
// Graph command
|
|
133
|
+
program
|
|
134
|
+
.command('graph')
|
|
135
|
+
.description('Show visual dependency graph of API interactions')
|
|
136
|
+
.action(async () => {
|
|
137
|
+
await showGraph();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Import command
|
|
141
|
+
program
|
|
142
|
+
.command('import')
|
|
143
|
+
.description('Import a request from a cURL command (opens editor if no command is provided)')
|
|
144
|
+
.allowUnknownOption()
|
|
145
|
+
.action(async (_, command) => {
|
|
146
|
+
if (command.args.length > 0) {
|
|
147
|
+
// Pass tokens directly to handleImport
|
|
148
|
+
await handleImport(command.args);
|
|
149
|
+
} else {
|
|
150
|
+
const inquirer = (await import('inquirer')).default;
|
|
151
|
+
const response = await inquirer.prompt([
|
|
152
|
+
{
|
|
153
|
+
type: 'editor',
|
|
154
|
+
name: 'curl',
|
|
155
|
+
message: 'Paste your cURL command here (opens your default editor):',
|
|
156
|
+
validate: (input) => input.trim().length > 0 || 'Please enter a cURL command'
|
|
157
|
+
}
|
|
158
|
+
]);
|
|
159
|
+
await handleImport(response.curl);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
108
163
|
// Graph command
|
|
109
164
|
/*
|
|
110
165
|
program
|
|
@@ -116,14 +171,12 @@ program
|
|
|
116
171
|
*/
|
|
117
172
|
|
|
118
173
|
// Stats command
|
|
119
|
-
/*
|
|
120
174
|
program
|
|
121
175
|
.command('stats')
|
|
122
176
|
.description('Show usage statistics')
|
|
123
177
|
.action(async () => {
|
|
124
178
|
await showStats();
|
|
125
179
|
});
|
|
126
|
-
*/
|
|
127
180
|
|
|
128
181
|
// Error handling
|
|
129
182
|
program.exitOverride();
|
package/package.json
CHANGED
package/src/executor.js
CHANGED
|
@@ -4,16 +4,20 @@ import ora from 'ora';
|
|
|
4
4
|
import { saveInteraction, loadEnv, saveEnv } from './storage.js';
|
|
5
5
|
import { formatResponse } from './formatter.js';
|
|
6
6
|
|
|
7
|
-
function applyEnvReplacements(data, envVars) {
|
|
7
|
+
function applyEnvReplacements(data, envVars, usedKeys = null) {
|
|
8
8
|
if (typeof data === 'string') {
|
|
9
9
|
return data.replace(/\{\{(.+?)\}\}/g, (match, key) => {
|
|
10
|
-
|
|
10
|
+
if (envVars[key] !== undefined) {
|
|
11
|
+
if (usedKeys) usedKeys.add(key);
|
|
12
|
+
return envVars[key];
|
|
13
|
+
}
|
|
14
|
+
return match;
|
|
11
15
|
});
|
|
12
16
|
}
|
|
13
17
|
if (typeof data === 'object' && data !== null) {
|
|
14
18
|
const newData = Array.isArray(data) ? [] : {};
|
|
15
19
|
for (const key in data) {
|
|
16
|
-
newData[key] = applyEnvReplacements(data[key], envVars);
|
|
20
|
+
newData[key] = applyEnvReplacements(data[key], envVars, usedKeys);
|
|
17
21
|
}
|
|
18
22
|
return newData;
|
|
19
23
|
}
|
|
@@ -52,8 +56,53 @@ export async function executeRequest(method, url, options = {}) {
|
|
|
52
56
|
const env = loadEnv();
|
|
53
57
|
const currentEnvVars = env.environments[env.current] || {};
|
|
54
58
|
|
|
59
|
+
const usedKeys = new Set();
|
|
60
|
+
const savedKeys = [];
|
|
61
|
+
|
|
55
62
|
// Apply replacements to URL
|
|
56
|
-
url = applyEnvReplacements(url, currentEnvVars);
|
|
63
|
+
url = applyEnvReplacements(url, currentEnvVars, usedKeys);
|
|
64
|
+
|
|
65
|
+
// Auto-BaseURL logic
|
|
66
|
+
if (currentEnvVars.baseUrl) {
|
|
67
|
+
let isRelative = false;
|
|
68
|
+
let pathPart = url;
|
|
69
|
+
|
|
70
|
+
// 1. Direct relative path
|
|
71
|
+
if (url.startsWith('/')) {
|
|
72
|
+
isRelative = true;
|
|
73
|
+
}
|
|
74
|
+
// 2. Windows Git Bash conversion: Detect "C:/..." style paths with no protocol
|
|
75
|
+
else if (process.platform === 'win32' && /^[a-zA-Z]:[/\\]/.test(url) && !url.includes('://')) {
|
|
76
|
+
isRelative = true;
|
|
77
|
+
// Extract the part after the drive letter and potential Git Bash root
|
|
78
|
+
// We look for common markers or just the first segment that looks like a path
|
|
79
|
+
// Most reliable for Git Bash: The user's path is at the end.
|
|
80
|
+
// We'll try to find the /api, /v1, etc., or just fallback to the full path after the first few segments
|
|
81
|
+
const segments = url.split(/[/\\]/);
|
|
82
|
+
const apiIdx = segments.findIndex(s => s === 'api' || s === 'v1' || s === 'v2');
|
|
83
|
+
if (apiIdx !== -1) {
|
|
84
|
+
pathPart = '/' + segments.slice(apiIdx).join('/');
|
|
85
|
+
} else {
|
|
86
|
+
// Fallback: If we can't find a marker, it's hard to guess safely,
|
|
87
|
+
// but we can try to strip the drive letter and first few segments
|
|
88
|
+
// In Git Bash, it's usually C:/Program Files/Git/api...
|
|
89
|
+
// Let's at least strip the drive letter root
|
|
90
|
+
pathPart = url.replace(/^[a-zA-Z]:/, '').replace(/\\/g, '/');
|
|
91
|
+
if (!pathPart.startsWith('/')) pathPart = '/' + pathPart;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// 3. No leading slash but doesn't look like a host (no dots, no protocol)
|
|
95
|
+
else if (!url.includes('://') && !url.includes('.') && !url.includes(':') && !url.startsWith('localhost')) {
|
|
96
|
+
isRelative = true;
|
|
97
|
+
pathPart = '/' + url;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (isRelative) {
|
|
101
|
+
const baseUrl = currentEnvVars.baseUrl;
|
|
102
|
+
const normalizedBaseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
|
|
103
|
+
url = normalizedBaseUrl + (pathPart.startsWith('/') ? pathPart : '/' + pathPart);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
57
106
|
|
|
58
107
|
// Parse headers
|
|
59
108
|
const headers = {};
|
|
@@ -61,7 +110,7 @@ export async function executeRequest(method, url, options = {}) {
|
|
|
61
110
|
options.header.forEach(h => {
|
|
62
111
|
const [key, ...valueParts] = h.split(':');
|
|
63
112
|
const headerValue = valueParts.join(':').trim();
|
|
64
|
-
headers[key.trim()] = applyEnvReplacements(headerValue, currentEnvVars);
|
|
113
|
+
headers[key.trim()] = applyEnvReplacements(headerValue, currentEnvVars, usedKeys);
|
|
65
114
|
});
|
|
66
115
|
}
|
|
67
116
|
|
|
@@ -70,7 +119,7 @@ export async function executeRequest(method, url, options = {}) {
|
|
|
70
119
|
if (options.data) {
|
|
71
120
|
let rawData = options.data;
|
|
72
121
|
// Apply replacements to raw data string before parsing
|
|
73
|
-
rawData = applyEnvReplacements(rawData, currentEnvVars);
|
|
122
|
+
rawData = applyEnvReplacements(rawData, currentEnvVars, usedKeys);
|
|
74
123
|
|
|
75
124
|
try {
|
|
76
125
|
body = JSON.parse(rawData);
|
|
@@ -133,6 +182,7 @@ export async function executeRequest(method, url, options = {}) {
|
|
|
133
182
|
const val = getDeep(response, responsePath);
|
|
134
183
|
if (val !== undefined) {
|
|
135
184
|
env.environments[env.current][envKey] = val;
|
|
185
|
+
savedKeys.push(envKey);
|
|
136
186
|
console.log(chalk.cyan(` ✨ Saved to env:`), chalk.white(`${envKey}=${val}`));
|
|
137
187
|
} else {
|
|
138
188
|
console.log(chalk.yellow(` ⚠️ Could not find path '${responsePath}' in response`));
|
|
@@ -155,6 +205,8 @@ export async function executeRequest(method, url, options = {}) {
|
|
|
155
205
|
data: response.data,
|
|
156
206
|
},
|
|
157
207
|
duration,
|
|
208
|
+
saves: savedKeys,
|
|
209
|
+
uses: Array.from(usedKeys)
|
|
158
210
|
});
|
|
159
211
|
|
|
160
212
|
console.log(chalk.gray('\n 💾 Interaction saved:'), chalk.white(interactionId));
|
package/src/graph.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { getAllInteractions } from './storage.js';
|
|
3
|
+
|
|
4
|
+
export async function showGraph() {
|
|
5
|
+
const interactions = getAllInteractions().reverse(); // Chronological order
|
|
6
|
+
|
|
7
|
+
if (interactions.length === 0) {
|
|
8
|
+
console.log(chalk.yellow('\n No interactions recorded yet.'));
|
|
9
|
+
console.log(chalk.gray(' Run some requests to see the dependency graph!\n'));
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// 1. Map: Variable -> Provider Interaction (Method + Path)
|
|
14
|
+
const variableProviders = {};
|
|
15
|
+
|
|
16
|
+
// 2. Interaction nodes with their connections
|
|
17
|
+
const nodes = [];
|
|
18
|
+
|
|
19
|
+
const getPath = (urlStr) => {
|
|
20
|
+
try {
|
|
21
|
+
const urlObj = new URL(urlStr);
|
|
22
|
+
return urlObj.pathname;
|
|
23
|
+
} catch (e) {
|
|
24
|
+
return urlStr.startsWith('http') ? urlStr : (urlStr.startsWith('/') ? urlStr : '/' + urlStr);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
interactions.forEach(int => {
|
|
29
|
+
const path = getPath(int.request.url);
|
|
30
|
+
const method = int.request.method;
|
|
31
|
+
const saves = int.metadata.saves || [];
|
|
32
|
+
const uses = int.metadata.uses || [];
|
|
33
|
+
|
|
34
|
+
// Track which variables this interaction provides
|
|
35
|
+
saves.forEach(v => {
|
|
36
|
+
variableProviders[v] = { method, path };
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
nodes.push({ method, path, saves, uses });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
console.log(chalk.cyan('\n 🕸️ API Dependency Graph:'));
|
|
43
|
+
console.log(chalk.gray(' (Shows how data flows between endpoints)\n'));
|
|
44
|
+
|
|
45
|
+
// Simple visualization
|
|
46
|
+
const seenPaths = new Set();
|
|
47
|
+
|
|
48
|
+
nodes.forEach((node, idx) => {
|
|
49
|
+
const nodeLabel = `${chalk.white(node.method)} ${chalk.gray(node.path)}`;
|
|
50
|
+
|
|
51
|
+
// Find dependencies based on 'uses'
|
|
52
|
+
const dependencies = node.uses.map(v => {
|
|
53
|
+
const provider = variableProviders[v];
|
|
54
|
+
return provider ? `[${v}] from ${provider.method} ${provider.path}` : null;
|
|
55
|
+
}).filter(Boolean);
|
|
56
|
+
|
|
57
|
+
// Render the node
|
|
58
|
+
if (dependencies.length > 0) {
|
|
59
|
+
console.log(` ${chalk.blue('⬇')} ${nodeLabel}`);
|
|
60
|
+
dependencies.forEach((dep, depIdx) => {
|
|
61
|
+
const branchToken = depIdx === dependencies.length - 1 ? '└─' : '├─';
|
|
62
|
+
console.log(` ${chalk.gray(branchToken)} ${chalk.yellow('uses')} ${dep}`);
|
|
63
|
+
});
|
|
64
|
+
} else {
|
|
65
|
+
console.log(` ${chalk.green('○')} ${nodeLabel}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (node.saves.length > 0) {
|
|
69
|
+
const saveToken = node.uses.length > 0 ? ' │' : ' ';
|
|
70
|
+
console.log(`${saveToken} ${chalk.magenta('↳')} ${chalk.gray('saves')} ${chalk.white(node.saves.join(', '))}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
console.log('');
|
|
74
|
+
});
|
|
75
|
+
}
|
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/snapshot.js
CHANGED
|
@@ -59,15 +59,27 @@ export async function compareSnapshots(tag1, tag2) {
|
|
|
59
59
|
const results = [];
|
|
60
60
|
let breakingChanges = 0;
|
|
61
61
|
|
|
62
|
-
//
|
|
62
|
+
// Helper to get path from URL string
|
|
63
|
+
const getPath = (urlStr) => {
|
|
64
|
+
try {
|
|
65
|
+
const urlObj = new URL(urlStr);
|
|
66
|
+
return urlObj.pathname;
|
|
67
|
+
} catch (e) {
|
|
68
|
+
// If it's already a path or invalid full URL, return as is
|
|
69
|
+
return urlStr.startsWith('http') ? urlStr : (urlStr.startsWith('/') ? urlStr : '/' + urlStr);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Domain-agnostic comparison: match by Path and Method
|
|
63
74
|
s2.interactions.forEach(int2 => {
|
|
64
|
-
const
|
|
75
|
+
const path2 = getPath(int2.url);
|
|
76
|
+
const int1 = s1.interactions.find(i => getPath(i.url) === path2 && i.method === int2.method);
|
|
65
77
|
|
|
66
78
|
if (!int1) {
|
|
67
79
|
results.push({
|
|
68
80
|
type: 'NEW',
|
|
69
81
|
method: int2.method,
|
|
70
|
-
url:
|
|
82
|
+
url: path2,
|
|
71
83
|
msg: chalk.blue('New interaction added')
|
|
72
84
|
});
|
|
73
85
|
return;
|
|
@@ -97,7 +109,7 @@ export async function compareSnapshots(tag1, tag2) {
|
|
|
97
109
|
results.push({
|
|
98
110
|
type: 'CHANGE',
|
|
99
111
|
method: int2.method,
|
|
100
|
-
url:
|
|
112
|
+
url: path2,
|
|
101
113
|
msg: diffs.join('\n ')
|
|
102
114
|
});
|
|
103
115
|
}
|
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';
|
|
@@ -39,6 +39,8 @@ export async function saveInteraction(interaction) {
|
|
|
39
39
|
response: interaction.response,
|
|
40
40
|
metadata: {
|
|
41
41
|
duration_ms: interaction.duration,
|
|
42
|
+
saves: interaction.saves || [], // Variables saved from this response
|
|
43
|
+
uses: interaction.uses || [], // Variables used in this request
|
|
42
44
|
},
|
|
43
45
|
};
|
|
44
46
|
|
|
@@ -112,6 +114,17 @@ export function getAllSnapshots() {
|
|
|
112
114
|
.map(f => f.replace('.json', ''));
|
|
113
115
|
}
|
|
114
116
|
|
|
117
|
+
export function clearAllInteractions() {
|
|
118
|
+
ensureKirooDir();
|
|
119
|
+
if (existsSync(INTERACTIONS_DIR)) {
|
|
120
|
+
const files = readdirSync(INTERACTIONS_DIR);
|
|
121
|
+
files.forEach(f => {
|
|
122
|
+
const filepath = join(INTERACTIONS_DIR, f);
|
|
123
|
+
rmSync(filepath, { force: true });
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
115
128
|
export function loadEnv() {
|
|
116
129
|
ensureKirooDir();
|
|
117
130
|
const data = readFileSync(ENV_FILE, 'utf8');
|