kiroo 0.3.4 → 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 CHANGED
@@ -6,7 +6,7 @@ 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
- // import { showGraph } from '../src/graph.js';
9
+ import { showGraph } from '../src/graph.js';
10
10
  import { initProject } from '../src/init.js';
11
11
  import { showStats } from '../src/stats.js';
12
12
  import { handleImport } from '../src/import.js';
@@ -17,7 +17,7 @@ const program = new Command();
17
17
  program
18
18
  .name('kiroo')
19
19
  .description('Git for API interactions. Record, replay, snapshot, and diff your APIs.')
20
- .version('0.3.4');
20
+ .version('0.4.0');
21
21
 
22
22
  // Init command
23
23
  program
@@ -129,6 +129,14 @@ snapshot
129
129
  await compareSnapshots(tag1, tag2);
130
130
  });
131
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
+
132
140
  // Import command
133
141
  program
134
142
  .command('import')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kiroo",
3
- "version": "0.3.4",
3
+ "version": "0.4.0",
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
@@ -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
- return envVars[key] !== undefined ? envVars[key] : match;
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,11 @@ 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);
57
64
 
58
65
  // Auto-BaseURL logic
59
66
  if (currentEnvVars.baseUrl) {
@@ -103,7 +110,7 @@ export async function executeRequest(method, url, options = {}) {
103
110
  options.header.forEach(h => {
104
111
  const [key, ...valueParts] = h.split(':');
105
112
  const headerValue = valueParts.join(':').trim();
106
- headers[key.trim()] = applyEnvReplacements(headerValue, currentEnvVars);
113
+ headers[key.trim()] = applyEnvReplacements(headerValue, currentEnvVars, usedKeys);
107
114
  });
108
115
  }
109
116
 
@@ -112,7 +119,7 @@ export async function executeRequest(method, url, options = {}) {
112
119
  if (options.data) {
113
120
  let rawData = options.data;
114
121
  // Apply replacements to raw data string before parsing
115
- rawData = applyEnvReplacements(rawData, currentEnvVars);
122
+ rawData = applyEnvReplacements(rawData, currentEnvVars, usedKeys);
116
123
 
117
124
  try {
118
125
  body = JSON.parse(rawData);
@@ -175,6 +182,7 @@ export async function executeRequest(method, url, options = {}) {
175
182
  const val = getDeep(response, responsePath);
176
183
  if (val !== undefined) {
177
184
  env.environments[env.current][envKey] = val;
185
+ savedKeys.push(envKey);
178
186
  console.log(chalk.cyan(` ✨ Saved to env:`), chalk.white(`${envKey}=${val}`));
179
187
  } else {
180
188
  console.log(chalk.yellow(` ⚠️ Could not find path '${responsePath}' in response`));
@@ -197,6 +205,8 @@ export async function executeRequest(method, url, options = {}) {
197
205
  data: response.data,
198
206
  },
199
207
  duration,
208
+ saves: savedKeys,
209
+ uses: Array.from(usedKeys)
200
210
  });
201
211
 
202
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/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
- // Simplistic comparison: match by URL and Method
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 int1 = s1.interactions.find(i => i.url === int2.url && i.method === int2.method);
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: int2.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: int2.url,
112
+ url: path2,
101
113
  msg: diffs.join('\n ')
102
114
  });
103
115
  }
package/src/storage.js CHANGED
@@ -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