@seanmozeik/tripwire 0.6.1 → 0.6.2
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/dist/tripwire-cli.js +1 -1
- package/dist/tripwire-cli.js.jsc +0 -0
- package/dist/tripwire.js +53 -53
- package/dist/tripwire.js.jsc +0 -0
- package/package.json +1 -1
- package/src/lib/grep-sanitize.ts +254 -0
- package/src/lib/rtk.ts +17 -1
package/dist/tripwire.js.jsc
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
interface Token {
|
|
2
|
+
readonly raw: string;
|
|
3
|
+
readonly value: string;
|
|
4
|
+
readonly start: number;
|
|
5
|
+
readonly end: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface SegmentPart {
|
|
9
|
+
readonly kind: 'segment';
|
|
10
|
+
readonly text: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface SeparatorPart {
|
|
14
|
+
readonly kind: 'separator';
|
|
15
|
+
readonly text: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type Part = SegmentPart | SeparatorPart;
|
|
19
|
+
|
|
20
|
+
const GREP_HEADS: ReadonlySet<string> = new Set(['grep', 'rg', 'egrep', 'fgrep']);
|
|
21
|
+
const COLLISION_SHORT: ReadonlySet<string> = new Set(['r', 'R', 'E']);
|
|
22
|
+
const COLLISION_LONG: ReadonlySet<string> = new Set([
|
|
23
|
+
'--recursive',
|
|
24
|
+
'--dereference-recursive',
|
|
25
|
+
'--extended-regexp',
|
|
26
|
+
]);
|
|
27
|
+
const VALUE_SHORT: ReadonlySet<string> = new Set(['A', 'B', 'C', 'm', 'd', 'D', 'e', 'f']);
|
|
28
|
+
|
|
29
|
+
const basename = (head: string): string => {
|
|
30
|
+
const slashIdx = head.lastIndexOf('/');
|
|
31
|
+
return slashIdx === -1 ? head : head.slice(slashIdx + 1);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const splitShellSegments = (command: string): Part[] => {
|
|
35
|
+
const parts: Part[] = [];
|
|
36
|
+
let quote: "'" | '"' | null = null;
|
|
37
|
+
let segmentStart = 0;
|
|
38
|
+
let i = 0;
|
|
39
|
+
|
|
40
|
+
while (i < command.length) {
|
|
41
|
+
const ch = command[i]!;
|
|
42
|
+
if (ch === '\\') {
|
|
43
|
+
i += 2;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (quote === "'") {
|
|
47
|
+
if (ch === "'") {
|
|
48
|
+
quote = null;
|
|
49
|
+
}
|
|
50
|
+
i++;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (quote === '"') {
|
|
54
|
+
if (ch === '"') {
|
|
55
|
+
quote = null;
|
|
56
|
+
}
|
|
57
|
+
i++;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (ch === "'" || ch === '"') {
|
|
61
|
+
quote = ch;
|
|
62
|
+
i++;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const two = command.slice(i, i + 2);
|
|
67
|
+
const op = two === '&&' || two === '||' || two === '|&' ? two : ch;
|
|
68
|
+
if (op === ';' || op === '&' || op === '|' || op === '&&' || op === '||' || op === '|&') {
|
|
69
|
+
if (i > segmentStart) {
|
|
70
|
+
parts.push({ kind: 'segment', text: command.slice(segmentStart, i) });
|
|
71
|
+
}
|
|
72
|
+
parts.push({ kind: 'separator', text: op });
|
|
73
|
+
i += op.length;
|
|
74
|
+
segmentStart = i;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
i++;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (segmentStart < command.length) {
|
|
81
|
+
parts.push({ kind: 'segment', text: command.slice(segmentStart) });
|
|
82
|
+
}
|
|
83
|
+
return parts;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const tokenizeSegment = (segment: string): Token[] => {
|
|
87
|
+
const tokens: Token[] = [];
|
|
88
|
+
let quote: "'" | '"' | null = null;
|
|
89
|
+
let raw = '';
|
|
90
|
+
let value = '';
|
|
91
|
+
let start = 0;
|
|
92
|
+
|
|
93
|
+
const flush = (end: number): void => {
|
|
94
|
+
if (raw.length === 0) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
tokens.push({ raw, value, start, end });
|
|
98
|
+
raw = '';
|
|
99
|
+
value = '';
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
for (let i = 0; i < segment.length; i++) {
|
|
103
|
+
const ch = segment[i]!;
|
|
104
|
+
if (raw.length === 0 && !/\s/u.test(ch)) {
|
|
105
|
+
start = i;
|
|
106
|
+
}
|
|
107
|
+
if (ch === '\\') {
|
|
108
|
+
raw += ch;
|
|
109
|
+
const next = segment[i + 1];
|
|
110
|
+
if (next !== undefined) {
|
|
111
|
+
raw += next;
|
|
112
|
+
value += next;
|
|
113
|
+
i++;
|
|
114
|
+
}
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (quote === "'") {
|
|
118
|
+
raw += ch;
|
|
119
|
+
if (ch === "'") {
|
|
120
|
+
quote = null;
|
|
121
|
+
} else {
|
|
122
|
+
value += ch;
|
|
123
|
+
}
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (quote === '"') {
|
|
127
|
+
raw += ch;
|
|
128
|
+
if (ch === '"') {
|
|
129
|
+
quote = null;
|
|
130
|
+
} else {
|
|
131
|
+
value += ch;
|
|
132
|
+
}
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (ch === "'" || ch === '"') {
|
|
136
|
+
raw += ch;
|
|
137
|
+
quote = ch;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (/\s/u.test(ch)) {
|
|
141
|
+
flush(i);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
raw += ch;
|
|
145
|
+
value += ch;
|
|
146
|
+
}
|
|
147
|
+
flush(segment.length);
|
|
148
|
+
return tokens;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const sanitizeShortFlag = (flag: string): string | null => {
|
|
152
|
+
let out = '-';
|
|
153
|
+
for (let i = 1; i < flag.length; i++) {
|
|
154
|
+
const letter = flag[i]!;
|
|
155
|
+
if (VALUE_SHORT.has(letter)) {
|
|
156
|
+
out += flag.slice(i);
|
|
157
|
+
return out.length === 1 ? null : out;
|
|
158
|
+
}
|
|
159
|
+
if (!COLLISION_SHORT.has(letter)) {
|
|
160
|
+
out += letter;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return out.length === 1 ? null : out;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const sanitizedTokenValues = (tokens: readonly Token[]): ReadonlyMap<Token, string | null> => {
|
|
167
|
+
const changed = new Map<Token, string | null>();
|
|
168
|
+
let optionsEnded = false;
|
|
169
|
+
let skipValueFor: string | null = null;
|
|
170
|
+
|
|
171
|
+
for (const token of tokens) {
|
|
172
|
+
if (skipValueFor !== null) {
|
|
173
|
+
skipValueFor = null;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (optionsEnded) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (token.value === '--') {
|
|
180
|
+
optionsEnded = true;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (COLLISION_LONG.has(token.value)) {
|
|
184
|
+
changed.set(token, null);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (token.value.startsWith('--')) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (token.value.startsWith('-') && token.value !== '-') {
|
|
191
|
+
const clean = sanitizeShortFlag(token.value);
|
|
192
|
+
if (clean !== token.value) {
|
|
193
|
+
changed.set(token, clean);
|
|
194
|
+
}
|
|
195
|
+
if (clean?.length === 2 && VALUE_SHORT.has(clean.at(-1)!)) {
|
|
196
|
+
skipValueFor = clean.at(-1)!;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return changed;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const applyTokenChanges = (
|
|
204
|
+
body: string,
|
|
205
|
+
tokens: readonly Token[],
|
|
206
|
+
changes: ReadonlyMap<Token, string | null>,
|
|
207
|
+
): string => {
|
|
208
|
+
let out = '';
|
|
209
|
+
let cursor = 0;
|
|
210
|
+
|
|
211
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
212
|
+
const token = tokens[i]!;
|
|
213
|
+
if (!changes.has(token)) {
|
|
214
|
+
out += body.slice(cursor, token.end);
|
|
215
|
+
cursor = token.end;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
const replacement = changes.get(token) ?? null;
|
|
219
|
+
out += body.slice(cursor, token.start);
|
|
220
|
+
if (replacement === null) {
|
|
221
|
+
cursor = token.end;
|
|
222
|
+
const next = tokens[i + 1];
|
|
223
|
+
if (next === undefined) {
|
|
224
|
+
out = out.trimEnd();
|
|
225
|
+
} else if (/\s$/u.test(out)) {
|
|
226
|
+
cursor = next.start;
|
|
227
|
+
}
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
out += replacement;
|
|
231
|
+
cursor = token.end;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return out + body.slice(cursor);
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const sanitizeSegment = (segment: string): string => {
|
|
238
|
+
const leading = /^\s*/u.exec(segment)?.[0] ?? '';
|
|
239
|
+
const trailing = /\s*$/u.exec(segment)?.[0] ?? '';
|
|
240
|
+
const body = segment.slice(leading.length, segment.length - trailing.length);
|
|
241
|
+
const tokens = tokenizeSegment(body);
|
|
242
|
+
const head = tokens[0];
|
|
243
|
+
if (head === undefined || !GREP_HEADS.has(basename(head.value))) {
|
|
244
|
+
return segment;
|
|
245
|
+
}
|
|
246
|
+
return `${leading}${applyTokenChanges(body, tokens, sanitizedTokenValues(tokens))}${trailing}`;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const sanitizeGrepFlags = (command: string): string =>
|
|
250
|
+
splitShellSegments(command)
|
|
251
|
+
.map((part) => (part.kind === 'segment' ? sanitizeSegment(part.text) : part.text))
|
|
252
|
+
.join('');
|
|
253
|
+
|
|
254
|
+
export { sanitizeGrepFlags };
|
package/src/lib/rtk.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { spawnSync } from 'node:child_process';
|
|
7
7
|
|
|
8
8
|
import type { RtkConfig } from './config';
|
|
9
|
+
import { sanitizeGrepFlags } from './grep-sanitize';
|
|
9
10
|
|
|
10
11
|
interface RtkOutput {
|
|
11
12
|
readonly updatedCommand?: string;
|
|
@@ -50,6 +51,21 @@ const findRtkBin = (config: RtkConfig): string | null => {
|
|
|
50
51
|
return null;
|
|
51
52
|
};
|
|
52
53
|
|
|
54
|
+
const sanitizeRtkEvent = (event: unknown): unknown => {
|
|
55
|
+
if (typeof event !== 'object' || event === null) {
|
|
56
|
+
return event;
|
|
57
|
+
}
|
|
58
|
+
const toolInput = (event as { readonly tool_input?: unknown }).tool_input;
|
|
59
|
+
if (typeof toolInput !== 'object' || toolInput === null) {
|
|
60
|
+
return event;
|
|
61
|
+
}
|
|
62
|
+
const command = (toolInput as { readonly command?: unknown }).command;
|
|
63
|
+
if (typeof command !== 'string') {
|
|
64
|
+
return event;
|
|
65
|
+
}
|
|
66
|
+
return { ...event, tool_input: { ...toolInput, command: sanitizeGrepFlags(command) } };
|
|
67
|
+
};
|
|
68
|
+
|
|
53
69
|
const runRtkRewrite = (event: unknown, config: RtkConfig, timeoutMs = 2000): RtkOutput => {
|
|
54
70
|
// If rtk is disabled, skip it
|
|
55
71
|
if (!config.enabled) {
|
|
@@ -62,7 +78,7 @@ const runRtkRewrite = (event: unknown, config: RtkConfig, timeoutMs = 2000): Rtk
|
|
|
62
78
|
return {};
|
|
63
79
|
}
|
|
64
80
|
|
|
65
|
-
const payload = JSON.stringify(event);
|
|
81
|
+
const payload = JSON.stringify(sanitizeRtkEvent(event));
|
|
66
82
|
const result = spawnSync(rtkBin, ['hook', 'claude'], {
|
|
67
83
|
input: payload,
|
|
68
84
|
encoding: 'utf8',
|