kiroo 0.8.0 → 0.9.5
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/README.md +386 -293
- package/bin/kiroo.js +412 -288
- package/package.json +2 -1
- package/src/analyze.js +568 -0
- package/src/bench.js +11 -4
- package/src/checker.js +26 -9
- package/src/config.js +109 -0
- package/src/deterministic.js +22 -0
- package/src/env.js +31 -3
- package/src/executor.js +18 -1
- package/src/export.js +560 -93
- package/src/formatter.js +18 -6
- package/src/init.js +80 -48
- package/src/lingo.js +55 -36
- package/src/proxy.js +140 -0
- package/src/replay.js +5 -4
- package/src/run.js +246 -0
- package/src/sanitizer.js +100 -0
- package/src/snapshot.js +76 -19
- package/src/stats.js +15 -5
- package/src/storage.js +223 -142
package/src/sanitizer.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
const REDACTED = '<REDACTED>';
|
|
2
|
+
|
|
3
|
+
const SENSITIVE_KEY_PATTERN =
|
|
4
|
+
/authorization|cookie|set-cookie|token|secret|password|passwd|pwd|api[-_]?key|x-api-key|client[-_]?secret|session|jwt|access[-_]?token|refresh[-_]?token/i;
|
|
5
|
+
|
|
6
|
+
function redactSensitiveString(value, redactedValue = REDACTED) {
|
|
7
|
+
if (typeof value !== 'string') {
|
|
8
|
+
return redactedValue;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (/^bearer\s+/i.test(value)) {
|
|
12
|
+
return `Bearer ${redactedValue}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (/^basic\s+/i.test(value)) {
|
|
16
|
+
return `Basic ${redactedValue}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return redactedValue;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function isSensitiveKey(key, sensitiveKeyPattern = SENSITIVE_KEY_PATTERN) {
|
|
23
|
+
return sensitiveKeyPattern.test(String(key || ''));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function redactSensitiveInUrl(urlValue, options = {}) {
|
|
27
|
+
const redactedValue = options.redactedValue || REDACTED;
|
|
28
|
+
const sensitiveKeyPattern = options.sensitiveKeyPattern || SENSITIVE_KEY_PATTERN;
|
|
29
|
+
|
|
30
|
+
if (typeof urlValue !== 'string' || !urlValue.includes('?')) {
|
|
31
|
+
return urlValue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const sanitizeParams = (url) => {
|
|
35
|
+
for (const key of Array.from(url.searchParams.keys())) {
|
|
36
|
+
if (isSensitiveKey(key, sensitiveKeyPattern)) {
|
|
37
|
+
url.searchParams.set(key, redactedValue);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return url.toString();
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
return sanitizeParams(new URL(urlValue));
|
|
45
|
+
} catch {
|
|
46
|
+
try {
|
|
47
|
+
const parsed = new URL(urlValue, 'http://kiroo.local');
|
|
48
|
+
sanitizeParams(parsed);
|
|
49
|
+
return `${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
50
|
+
} catch {
|
|
51
|
+
return urlValue;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function sanitizeValue(value, currentKey = '', options = {}) {
|
|
57
|
+
const redactedValue = options.redactedValue || REDACTED;
|
|
58
|
+
const sensitiveKeyPattern = options.sensitiveKeyPattern || SENSITIVE_KEY_PATTERN;
|
|
59
|
+
|
|
60
|
+
if (isSensitiveKey(currentKey, sensitiveKeyPattern)) {
|
|
61
|
+
return redactSensitiveString(value, redactedValue);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (typeof value === 'string') {
|
|
65
|
+
if (currentKey.toLowerCase() === 'url') {
|
|
66
|
+
return redactSensitiveInUrl(value, options);
|
|
67
|
+
}
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (Array.isArray(value)) {
|
|
72
|
+
return value.map((item) => sanitizeValue(item, currentKey, options));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (value && typeof value === 'object') {
|
|
76
|
+
const out = {};
|
|
77
|
+
for (const [key, nestedValue] of Object.entries(value)) {
|
|
78
|
+
out[key] = sanitizeValue(nestedValue, key, options);
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function sanitizeInteractionRecord(record, options = {}) {
|
|
87
|
+
const mergedOptions = {
|
|
88
|
+
enabled: options.enabled !== false,
|
|
89
|
+
redactedValue: options.redactedValue || REDACTED,
|
|
90
|
+
sensitiveKeyPattern: options.sensitiveKeyPattern || SENSITIVE_KEY_PATTERN
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
if (!mergedOptions.enabled) {
|
|
94
|
+
return record;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return sanitizeValue(record, '', mergedOptions);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export { REDACTED };
|
package/src/snapshot.js
CHANGED
|
@@ -60,7 +60,7 @@ export async function compareSnapshots(tag1, tag2, lang) {
|
|
|
60
60
|
console.log(chalk.magenta(` 🌍 Translating output to: ${chalk.white(lang.toUpperCase())} using Lingo.dev...`));
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
const
|
|
63
|
+
const resultMap = new Map();
|
|
64
64
|
let breakingChanges = 0;
|
|
65
65
|
|
|
66
66
|
// Helper to get path from URL string
|
|
@@ -74,27 +74,66 @@ export async function compareSnapshots(tag1, tag2, lang) {
|
|
|
74
74
|
}
|
|
75
75
|
};
|
|
76
76
|
|
|
77
|
+
const compareByMethodAndPath = (a, b) => {
|
|
78
|
+
const pathA = getPath(a.url || '');
|
|
79
|
+
const pathB = getPath(b.url || '');
|
|
80
|
+
const methodA = String(a.method || '').toUpperCase();
|
|
81
|
+
const methodB = String(b.method || '').toUpperCase();
|
|
82
|
+
if (methodA !== methodB) return methodA.localeCompare(methodB);
|
|
83
|
+
return pathA.localeCompare(pathB);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const s1Interactions = [...s1.interactions].sort(compareByMethodAndPath);
|
|
87
|
+
const s2Interactions = [...s2.interactions].sort(compareByMethodAndPath);
|
|
88
|
+
const consumedS1Indexes = new Set();
|
|
89
|
+
|
|
90
|
+
const isBreakingStatusChange = (beforeStatus, afterStatus) => {
|
|
91
|
+
const before2xx = beforeStatus >= 200 && beforeStatus < 300;
|
|
92
|
+
const after3xx = afterStatus >= 300 && afterStatus < 400;
|
|
93
|
+
const after4xx5xx = afterStatus >= 400;
|
|
94
|
+
|
|
95
|
+
if (before2xx && afterStatus === 304) return false;
|
|
96
|
+
if (before2xx && after3xx) return false;
|
|
97
|
+
if (before2xx && after4xx5xx) return true;
|
|
98
|
+
return beforeStatus !== afterStatus;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const addResult = (type, method, url, msg) => {
|
|
102
|
+
const cleanMethod = String(method || '').toUpperCase();
|
|
103
|
+
const key = `${type}|${cleanMethod}|${url}`;
|
|
104
|
+
if (!resultMap.has(key)) {
|
|
105
|
+
resultMap.set(key, { type, method: cleanMethod, url, messages: [], occurrences: 0 });
|
|
106
|
+
}
|
|
107
|
+
const row = resultMap.get(key);
|
|
108
|
+
row.occurrences += 1;
|
|
109
|
+
if (msg && !row.messages.includes(msg)) {
|
|
110
|
+
row.messages.push(msg);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
77
114
|
// Domain-agnostic comparison: match by Path and Method
|
|
78
|
-
|
|
115
|
+
s2Interactions.forEach(int2 => {
|
|
79
116
|
const path2 = getPath(int2.url);
|
|
80
|
-
const
|
|
117
|
+
const candidates = s1Interactions
|
|
118
|
+
.map((item, index) => ({ item, index }))
|
|
119
|
+
.filter(({ item, index }) => !consumedS1Indexes.has(index) && getPath(item.url) === path2 && item.method === int2.method);
|
|
120
|
+
const match = candidates.find(({ item }) => item.id && int2.id && item.id === int2.id) || candidates[0];
|
|
121
|
+
const int1 = match?.item;
|
|
81
122
|
|
|
82
123
|
if (!int1) {
|
|
83
|
-
|
|
84
|
-
type: 'NEW',
|
|
85
|
-
method: int2.method,
|
|
86
|
-
url: path2,
|
|
87
|
-
msg: chalk.blue('New interaction added')
|
|
88
|
-
});
|
|
124
|
+
addResult('NEW', int2.method, path2, 'New interaction added');
|
|
89
125
|
return;
|
|
90
126
|
}
|
|
127
|
+
consumedS1Indexes.add(match.index);
|
|
91
128
|
|
|
92
129
|
const diffs = [];
|
|
93
130
|
|
|
94
131
|
// Compare status
|
|
95
132
|
if (int1.response.status !== int2.response.status) {
|
|
96
133
|
diffs.push(`Status: ${chalk.gray(int1.response.status)} → ${chalk.red(int2.response.status)}`);
|
|
97
|
-
|
|
134
|
+
if (isBreakingStatusChange(int1.response.status, int2.response.status)) {
|
|
135
|
+
breakingChanges++;
|
|
136
|
+
}
|
|
98
137
|
}
|
|
99
138
|
|
|
100
139
|
// Helper for deep structural comparison
|
|
@@ -154,15 +193,19 @@ export async function compareSnapshots(tag1, tag2, lang) {
|
|
|
154
193
|
}
|
|
155
194
|
}
|
|
156
195
|
if (diffs.length > 0) {
|
|
157
|
-
|
|
158
|
-
type: 'CHANGE',
|
|
159
|
-
method: int2.method,
|
|
160
|
-
url: path2,
|
|
161
|
-
msg: diffs.join('\n ')
|
|
162
|
-
});
|
|
196
|
+
addResult('CHANGE', int2.method, path2, diffs.join('\n '));
|
|
163
197
|
}
|
|
164
198
|
});
|
|
165
199
|
|
|
200
|
+
s1Interactions.forEach((int1, index) => {
|
|
201
|
+
if (consumedS1Indexes.has(index)) return;
|
|
202
|
+
const path1 = getPath(int1.url);
|
|
203
|
+
addResult('REMOVED', int1.method, path1, 'Interaction removed in target snapshot');
|
|
204
|
+
breakingChanges++;
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const results = [...resultMap.values()];
|
|
208
|
+
|
|
166
209
|
if (results.length === 0) {
|
|
167
210
|
let finalMsg = 'No differences detected. Your API is stable!';
|
|
168
211
|
if (lang) finalMsg = await translateText(finalMsg, lang);
|
|
@@ -170,8 +213,18 @@ export async function compareSnapshots(tag1, tag2, lang) {
|
|
|
170
213
|
} else {
|
|
171
214
|
console.log('');
|
|
172
215
|
|
|
173
|
-
|
|
174
|
-
|
|
216
|
+
const sortedResults = [...results].sort((a, b) => {
|
|
217
|
+
const methodA = String(a.method || '').toUpperCase();
|
|
218
|
+
const methodB = String(b.method || '').toUpperCase();
|
|
219
|
+
if (methodA !== methodB) return methodA.localeCompare(methodB);
|
|
220
|
+
return String(a.url || '').localeCompare(String(b.url || ''));
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
for (const res of sortedResults) {
|
|
224
|
+
let printMsg = res.messages.join('\n ');
|
|
225
|
+
if (res.occurrences > 1 && (res.type === 'NEW' || res.type === 'REMOVED')) {
|
|
226
|
+
printMsg += `\n (${res.occurrences} occurrences)`;
|
|
227
|
+
}
|
|
175
228
|
if (lang) {
|
|
176
229
|
// Basic translation hook for individual diff items (stripping ansi)
|
|
177
230
|
const cleanMsg = printMsg.replace(/\x1B\[[0-9;]*m/g, '');
|
|
@@ -179,7 +232,11 @@ export async function compareSnapshots(tag1, tag2, lang) {
|
|
|
179
232
|
printMsg = chalk.yellow('[Translated] ') + translatedMsg;
|
|
180
233
|
}
|
|
181
234
|
|
|
182
|
-
const symbol = res.type === 'NEW'
|
|
235
|
+
const symbol = res.type === 'NEW'
|
|
236
|
+
? chalk.blue('+')
|
|
237
|
+
: res.type === 'REMOVED'
|
|
238
|
+
? chalk.red('-')
|
|
239
|
+
: chalk.yellow('⚠️');
|
|
183
240
|
console.log(` ${symbol} ${chalk.white(res.method)} ${chalk.gray(res.url)}`);
|
|
184
241
|
console.log(` ${printMsg}`);
|
|
185
242
|
}
|
package/src/stats.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import Table from 'cli-table3';
|
|
3
3
|
import { getAllInteractions } from './storage.js';
|
|
4
|
+
import { translateText } from './lingo.js';
|
|
4
5
|
|
|
5
|
-
export function showStats() {
|
|
6
|
+
export async function showStats(options = {}) {
|
|
7
|
+
const lang = options.lang;
|
|
6
8
|
const interactions = getAllInteractions();
|
|
7
9
|
|
|
8
10
|
if (interactions.length === 0) {
|
|
@@ -11,7 +13,9 @@ export function showStats() {
|
|
|
11
13
|
return;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
let title = '📊 Kiroo Analytics Dashboard';
|
|
17
|
+
if (lang) title = await translateText(title, lang);
|
|
18
|
+
console.log(chalk.cyan.bold(`\n ${title}\n`));
|
|
15
19
|
|
|
16
20
|
// 1. General Metrics
|
|
17
21
|
const total = interactions.length;
|
|
@@ -26,7 +30,9 @@ export function showStats() {
|
|
|
26
30
|
{ [chalk.white('Avg. Duration')]: chalk.white(avgDuration + 'ms') }
|
|
27
31
|
);
|
|
28
32
|
|
|
29
|
-
|
|
33
|
+
let generalHeader = '● General Performance';
|
|
34
|
+
if (lang) generalHeader = await translateText(generalHeader, lang);
|
|
35
|
+
console.log(chalk.white.bold(` ${generalHeader}`));
|
|
30
36
|
console.log(generalTable.toString());
|
|
31
37
|
|
|
32
38
|
// 2. Method Distribution
|
|
@@ -45,7 +51,9 @@ export function showStats() {
|
|
|
45
51
|
methodTable.push([chalk.white(method), chalk.white(count), chalk.gray(percentage)]);
|
|
46
52
|
});
|
|
47
53
|
|
|
48
|
-
|
|
54
|
+
let distHeader = '● Request Distribution';
|
|
55
|
+
if (lang) distHeader = await translateText(distHeader, lang);
|
|
56
|
+
console.log(chalk.white.bold(`\n ${distHeader}`));
|
|
49
57
|
console.log(methodTable.toString());
|
|
50
58
|
|
|
51
59
|
// 3. Slowest Endpoints
|
|
@@ -67,7 +75,9 @@ export function showStats() {
|
|
|
67
75
|
]);
|
|
68
76
|
});
|
|
69
77
|
|
|
70
|
-
|
|
78
|
+
let slowHeader = '● Top 5 Slowest Endpoints (Bottlenecks)';
|
|
79
|
+
if (lang) slowHeader = await translateText(slowHeader, lang);
|
|
80
|
+
console.log(chalk.white.bold(`\n ${slowHeader}`));
|
|
71
81
|
console.log(slowTable.toString());
|
|
72
82
|
console.log('');
|
|
73
83
|
}
|
package/src/storage.js
CHANGED
|
@@ -1,142 +1,223 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, rmSync } from 'fs';
|
|
2
|
-
import { join } from 'path';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export function
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (!existsSync(
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
return
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, rmSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { sanitizeInteractionRecord } from './sanitizer.js';
|
|
4
|
+
import { loadKirooConfig } from './config.js';
|
|
5
|
+
import { stableJSONStringify } from './deterministic.js';
|
|
6
|
+
|
|
7
|
+
const KIROO_DIR = '.kiroo';
|
|
8
|
+
const INTERACTIONS_DIR = join(KIROO_DIR, 'interactions');
|
|
9
|
+
const SNAPSHOTS_DIR = join(KIROO_DIR, 'snapshots');
|
|
10
|
+
const ENV_FILE = join(KIROO_DIR, 'env.json');
|
|
11
|
+
|
|
12
|
+
function getPersistenceSettings() {
|
|
13
|
+
const config = loadKirooConfig();
|
|
14
|
+
const redaction = config.settings?.redaction || {};
|
|
15
|
+
const determinism = config.settings?.determinism || {};
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
redactEnabled: redaction.enabled !== false,
|
|
19
|
+
redactedValue: redaction.redactedValue || '<REDACTED>',
|
|
20
|
+
sortKeys: determinism.sortKeys !== false
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function stringifyForPersistence(value, sortKeysEnabled) {
|
|
25
|
+
if (sortKeysEnabled) {
|
|
26
|
+
return stableJSONStringify(value, 2);
|
|
27
|
+
}
|
|
28
|
+
return JSON.stringify(value, null, 2);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function ensureKirooDir() {
|
|
32
|
+
if (!existsSync(KIROO_DIR)) {
|
|
33
|
+
mkdirSync(KIROO_DIR);
|
|
34
|
+
}
|
|
35
|
+
if (!existsSync(INTERACTIONS_DIR)) {
|
|
36
|
+
mkdirSync(INTERACTIONS_DIR);
|
|
37
|
+
}
|
|
38
|
+
if (!existsSync(SNAPSHOTS_DIR)) {
|
|
39
|
+
mkdirSync(SNAPSHOTS_DIR);
|
|
40
|
+
}
|
|
41
|
+
if (!existsSync(ENV_FILE)) {
|
|
42
|
+
writeFileSync(ENV_FILE, JSON.stringify({ current: 'default', environments: { default: {} } }, null, 2));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const GITIGNORE_FILE = join(KIROO_DIR, '.gitignore');
|
|
46
|
+
if (!existsSync(GITIGNORE_FILE)) {
|
|
47
|
+
writeFileSync(GITIGNORE_FILE, 'env.json\n');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function saveInteraction(interaction) {
|
|
52
|
+
ensureKirooDir();
|
|
53
|
+
const settings = getPersistenceSettings();
|
|
54
|
+
|
|
55
|
+
const timestamp = new Date().toISOString();
|
|
56
|
+
const id = timestamp.replace(/[:.]/g, '-');
|
|
57
|
+
|
|
58
|
+
const interactionData = {
|
|
59
|
+
id,
|
|
60
|
+
timestamp,
|
|
61
|
+
request: {
|
|
62
|
+
method: interaction.method,
|
|
63
|
+
url: interaction.url,
|
|
64
|
+
headers: interaction.headers,
|
|
65
|
+
body: interaction.body,
|
|
66
|
+
},
|
|
67
|
+
response: interaction.response,
|
|
68
|
+
metadata: {
|
|
69
|
+
duration_ms: interaction.duration,
|
|
70
|
+
saves: interaction.saves || [], // Variables saved from this response
|
|
71
|
+
uses: interaction.uses || [], // Variables used in this request
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const sanitizedInteractionData = sanitizeInteractionRecord(interactionData, {
|
|
76
|
+
enabled: settings.redactEnabled,
|
|
77
|
+
redactedValue: settings.redactedValue
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const filename = `${id}.json`;
|
|
81
|
+
const filepath = join(INTERACTIONS_DIR, filename);
|
|
82
|
+
|
|
83
|
+
writeFileSync(filepath, stringifyForPersistence(sanitizedInteractionData, settings.sortKeys));
|
|
84
|
+
|
|
85
|
+
return id;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function loadInteraction(id) {
|
|
89
|
+
const filepath = join(INTERACTIONS_DIR, `${id}.json`);
|
|
90
|
+
|
|
91
|
+
if (!existsSync(filepath)) {
|
|
92
|
+
throw new Error(`Interaction not found: ${id}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const data = readFileSync(filepath, 'utf8');
|
|
96
|
+
return JSON.parse(data);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function getAllInteractions() {
|
|
100
|
+
ensureKirooDir();
|
|
101
|
+
|
|
102
|
+
if (!existsSync(INTERACTIONS_DIR)) {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const files = readdirSync(INTERACTIONS_DIR)
|
|
107
|
+
.filter(f => f.endsWith('.json'))
|
|
108
|
+
.sort()
|
|
109
|
+
.reverse(); // Most recent first
|
|
110
|
+
|
|
111
|
+
return files.map(f => {
|
|
112
|
+
const filepath = join(INTERACTIONS_DIR, f);
|
|
113
|
+
const data = readFileSync(filepath, 'utf8');
|
|
114
|
+
return JSON.parse(data);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function saveSnapshotData(tag, data) {
|
|
119
|
+
ensureKirooDir();
|
|
120
|
+
const settings = getPersistenceSettings();
|
|
121
|
+
|
|
122
|
+
const filename = `${tag}.json`;
|
|
123
|
+
const filepath = join(SNAPSHOTS_DIR, filename);
|
|
124
|
+
const sanitizedSnapshot = sanitizeInteractionRecord(data, {
|
|
125
|
+
enabled: settings.redactEnabled,
|
|
126
|
+
redactedValue: settings.redactedValue
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
writeFileSync(filepath, stringifyForPersistence(sanitizedSnapshot, settings.sortKeys));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function loadSnapshotData(tag) {
|
|
133
|
+
const filepath = join(SNAPSHOTS_DIR, `${tag}.json`);
|
|
134
|
+
|
|
135
|
+
if (!existsSync(filepath)) {
|
|
136
|
+
throw new Error(`Snapshot not found: ${tag}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const data = readFileSync(filepath, 'utf8');
|
|
140
|
+
return JSON.parse(data);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function getAllSnapshots() {
|
|
144
|
+
ensureKirooDir();
|
|
145
|
+
|
|
146
|
+
if (!existsSync(SNAPSHOTS_DIR)) {
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return readdirSync(SNAPSHOTS_DIR)
|
|
151
|
+
.filter(f => f.endsWith('.json'))
|
|
152
|
+
.sort((a, b) => a.localeCompare(b))
|
|
153
|
+
.map(f => f.replace('.json', ''));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function clearAllInteractions() {
|
|
157
|
+
ensureKirooDir();
|
|
158
|
+
if (existsSync(INTERACTIONS_DIR)) {
|
|
159
|
+
const files = readdirSync(INTERACTIONS_DIR);
|
|
160
|
+
files.forEach(f => {
|
|
161
|
+
const filepath = join(INTERACTIONS_DIR, f);
|
|
162
|
+
rmSync(filepath, { force: true });
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function scrubDirectory(directoryPath, { dryRun = false } = {}) {
|
|
168
|
+
const settings = getPersistenceSettings();
|
|
169
|
+
|
|
170
|
+
if (!existsSync(directoryPath)) {
|
|
171
|
+
return { scanned: 0, updated: 0 };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const files = readdirSync(directoryPath).filter((f) => f.endsWith('.json'));
|
|
175
|
+
let updated = 0;
|
|
176
|
+
|
|
177
|
+
for (const fileName of files) {
|
|
178
|
+
const filePath = join(directoryPath, fileName);
|
|
179
|
+
const originalRaw = readFileSync(filePath, 'utf8');
|
|
180
|
+
const originalData = JSON.parse(originalRaw);
|
|
181
|
+
const sanitizedData = sanitizeInteractionRecord(originalData, {
|
|
182
|
+
enabled: settings.redactEnabled,
|
|
183
|
+
redactedValue: settings.redactedValue
|
|
184
|
+
});
|
|
185
|
+
const sanitizedRaw = stringifyForPersistence(sanitizedData, settings.sortKeys);
|
|
186
|
+
|
|
187
|
+
if (originalRaw !== sanitizedRaw) {
|
|
188
|
+
updated += 1;
|
|
189
|
+
if (!dryRun) {
|
|
190
|
+
writeFileSync(filePath, sanitizedRaw);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return { scanned: files.length, updated };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function scrubStoredData(options = {}) {
|
|
199
|
+
ensureKirooDir();
|
|
200
|
+
const { dryRun = false } = options;
|
|
201
|
+
|
|
202
|
+
const interactions = scrubDirectory(INTERACTIONS_DIR, { dryRun });
|
|
203
|
+
const snapshots = scrubDirectory(SNAPSHOTS_DIR, { dryRun });
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
interactions,
|
|
207
|
+
snapshots,
|
|
208
|
+
totalUpdated: interactions.updated + snapshots.updated,
|
|
209
|
+
dryRun,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function loadEnv() {
|
|
214
|
+
ensureKirooDir();
|
|
215
|
+
const data = readFileSync(ENV_FILE, 'utf8');
|
|
216
|
+
return JSON.parse(data);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function saveEnv(data) {
|
|
220
|
+
ensureKirooDir();
|
|
221
|
+
const settings = getPersistenceSettings();
|
|
222
|
+
writeFileSync(ENV_FILE, stringifyForPersistence(data, settings.sortKeys));
|
|
223
|
+
}
|