jawere 1.0.13

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.
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Create a prompt function appropriate for the current terminal.
3
+ * Returns multilinePrompt for TTYs, simplePrompt for pipes/files.
4
+ */
5
+ export declare function createPrompt(): {
6
+ prompt: () => Promise<string>;
7
+ enableBracketedPaste: () => void;
8
+ disableBracketedPaste: () => void;
9
+ };
package/dist/prompt.js ADDED
@@ -0,0 +1,325 @@
1
+ // src/prompt.ts — Multiline prompt with Shift+Enter & paste support
2
+ // Extracted from index.ts to isolate complex terminal handling for testability.
3
+ import * as readline from 'readline';
4
+ // Gruvbox colors
5
+ const G_GRAY = '\x1b[38;2;146;131;116m';
6
+ const R = '\x1b[0m';
7
+ const PROMPT = `${G_GRAY}>${R} `;
8
+ const CONT = ' ';
9
+ // ── Long paste threshold ────────────────────────────────────────────
10
+ // Pastes exceeding this many lines or characters are stored and
11
+ // replaced with a [paste #N] placeholder to keep the prompt responsive.
12
+ const PASTE_LINE_LIMIT = 20;
13
+ const PASTE_CHAR_LIMIT = 500;
14
+ /**
15
+ * Simple fallback prompt for piped/non-TTY input.
16
+ */
17
+ function simplePrompt() {
18
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
19
+ return new Promise((resolve) => {
20
+ rl.question('> ', (answer) => { rl.close(); resolve(answer); });
21
+ });
22
+ }
23
+ /**
24
+ * Full-featured multiline prompt with:
25
+ * - Shift+Enter for newlines (kitty: CSI 13;2u / xterm: CSI 13;2~)
26
+ * - Bracketed paste mode (multi-line paste support)
27
+ * - Long pastes stored as [paste #N] placeholders to avoid UI churn
28
+ * - Arrow keys, Home, End, Delete, Backspace
29
+ * - Ctrl+C to cancel, Ctrl+D to exit on empty line
30
+ *
31
+ * This is raw-mode terminal handling — fragile but necessary for a good UX.
32
+ * Isolating it here makes it easier to test, debug, and replace.
33
+ */
34
+ function multilinePrompt() {
35
+ return new Promise((resolve) => {
36
+ const lines = [''];
37
+ let row = 0;
38
+ let col = 0;
39
+ // ── Paste state ──────────────────────────────────────────────
40
+ let pasteMode = false;
41
+ let pasteBuf = '';
42
+ // Map of placeholder -> actual content for long pastes
43
+ const storedPastes = [];
44
+ let pasteCounter = 0;
45
+ process.stdout.write('\n');
46
+ process.stdout.write(PROMPT);
47
+ const rawOn = () => {
48
+ if (process.stdin.isTTY && typeof process.stdin.setRawMode === 'function') {
49
+ process.stdin.setRawMode(true);
50
+ }
51
+ };
52
+ const rawOff = () => {
53
+ if (process.stdin.isTTY && typeof process.stdin.setRawMode === 'function') {
54
+ process.stdin.setRawMode(false);
55
+ }
56
+ };
57
+ rawOn();
58
+ // ── Redraw helper ────────────────────────────────────────────
59
+ // Repaints the entire prompt and all continuation lines, then
60
+ // repositions the cursor at (row, col).
61
+ const redraw = () => {
62
+ // Move cursor up to the first prompt line
63
+ process.stdout.write(`\x1b[${row}A\r`);
64
+ // Clear from cursor to end of screen
65
+ process.stdout.write('\x1b[0J');
66
+ // Print first line with prompt prefix
67
+ process.stdout.write(PROMPT + lines[0]);
68
+ // Print continuation lines
69
+ for (let i = 1; i < lines.length; i++) {
70
+ process.stdout.write('\r\n' + CONT + lines[i]);
71
+ }
72
+ // Reposition cursor
73
+ const moveUp = lines.length - 1 - row;
74
+ if (moveUp > 0)
75
+ process.stdout.write(`\x1b[${moveUp}A`);
76
+ process.stdout.write('\r');
77
+ const prefixLen = row === 0 ? PROMPT.length : CONT.length;
78
+ if (col > 0)
79
+ process.stdout.write(`\x1b[${prefixLen + col}C`);
80
+ };
81
+ const cleanup = () => {
82
+ process.stdin.removeListener('data', onData);
83
+ rawOff();
84
+ };
85
+ // ── Expand paste placeholders ────────────────────────────────
86
+ // Replace [paste #N] markers with actual stored paste content.
87
+ const expandPastes = (text) => {
88
+ let result = text;
89
+ for (let i = 0; i < storedPastes.length; i++) {
90
+ const placeholder = `[paste #${i + 1}]`;
91
+ // Use split/join for simple replacement (handles multiple occurrences)
92
+ result = result.split(placeholder).join(storedPastes[i]);
93
+ }
94
+ return result;
95
+ };
96
+ // ── Process a completed paste ────────────────────────────────
97
+ const processPaste = (content) => {
98
+ const lineCount = content.split('\n').length;
99
+ const charCount = content.length;
100
+ // Decide: render inline or store as placeholder?
101
+ if (lineCount > PASTE_LINE_LIMIT || charCount > PASTE_CHAR_LIMIT) {
102
+ // ── Long paste → placeholder ──
103
+ pasteCounter++;
104
+ storedPastes.push(content);
105
+ const placeholder = `[paste #${pasteCounter}]`;
106
+ const before = lines[row].slice(0, col);
107
+ const after = lines[row].slice(col);
108
+ lines[row] = before + placeholder + after;
109
+ col += placeholder.length;
110
+ process.stdout.write(placeholder);
111
+ if (after.length > 0)
112
+ redraw();
113
+ }
114
+ else {
115
+ // ── Short paste → render inline ──
116
+ const before = lines[row].slice(0, col);
117
+ const after = lines[row].slice(col);
118
+ const pasteLines = content.split('\n');
119
+ if (pasteLines.length === 1) {
120
+ // Single-line paste
121
+ lines[row] = before + pasteLines[0] + after;
122
+ col += pasteLines[0].length;
123
+ process.stdout.write(pasteLines[0]);
124
+ if (after.length > 0)
125
+ redraw();
126
+ }
127
+ else {
128
+ // Multi-line paste
129
+ lines[row] = before + pasteLines[0];
130
+ for (let i = 1; i < pasteLines.length; i++) {
131
+ lines.splice(row + i, 0, pasteLines[i]);
132
+ }
133
+ lines[row + pasteLines.length - 1] += after;
134
+ row += pasteLines.length - 1;
135
+ col = pasteLines[pasteLines.length - 1].length;
136
+ redraw();
137
+ }
138
+ }
139
+ };
140
+ // ── Data handler ─────────────────────────────────────────────
141
+ const onData = (buf) => {
142
+ const s = buf.toString();
143
+ // ── Bracketed paste start ─────────────────────────────────
144
+ if (!pasteMode && s.startsWith('\x1b[200~')) {
145
+ pasteMode = true;
146
+ // Everything after the 6-char start marker goes into pasteBuf
147
+ pasteBuf = s.slice(6);
148
+ // Check if the entire paste (including end marker) is in this chunk.
149
+ // This is the critical fix: we must search pasteBuf, not just s.
150
+ const endIdx = pasteBuf.indexOf('\x1b[201~');
151
+ if (endIdx !== -1) {
152
+ const content = pasteBuf.slice(0, endIdx);
153
+ const rest = pasteBuf.slice(endIdx + 6);
154
+ pasteBuf = '';
155
+ pasteMode = false;
156
+ processPaste(content);
157
+ if (rest)
158
+ onData(Buffer.from(rest));
159
+ }
160
+ return;
161
+ }
162
+ // ── Accumulating paste data ───────────────────────────────
163
+ if (pasteMode) {
164
+ pasteBuf += s;
165
+ // Search pasteBuf for the end marker (not just s — this is the fix)
166
+ const endIdx = pasteBuf.indexOf('\x1b[201~');
167
+ if (endIdx !== -1) {
168
+ const content = pasteBuf.slice(0, endIdx);
169
+ const rest = pasteBuf.slice(endIdx + 6);
170
+ pasteBuf = '';
171
+ pasteMode = false;
172
+ processPaste(content);
173
+ if (rest)
174
+ onData(Buffer.from(rest));
175
+ }
176
+ // If end marker not found, keep accumulating — no action needed
177
+ return;
178
+ }
179
+ // ── Shift+Enter (kitty: CSI 13;2u, xterm: CSI 13;2~) ─────
180
+ if (s === '\x1b[13;2u' || s === '\x1b[13;2~') {
181
+ const before = lines[row].slice(0, col);
182
+ const after = lines[row].slice(col);
183
+ lines[row] = before;
184
+ lines.splice(row + 1, 0, after);
185
+ row++;
186
+ col = 0;
187
+ process.stdout.write('\r\n' + CONT);
188
+ return;
189
+ }
190
+ // ── Enter ─────────────────────────────────────────────────
191
+ if (s === '\r' || s === '\n') {
192
+ cleanup();
193
+ process.stdout.write('\r\n');
194
+ const raw = lines.join('\n');
195
+ const expanded = expandPastes(raw);
196
+ resolve(expanded);
197
+ return;
198
+ }
199
+ // ── Ctrl+C ────────────────────────────────────────────────
200
+ if (s === '\x03') {
201
+ cleanup();
202
+ process.stdout.write('^C\r\n');
203
+ resolve('');
204
+ return;
205
+ }
206
+ // ── Ctrl+D on empty line ──────────────────────────────────
207
+ if (s === '\x04' && lines.length === 1 && lines[0].length === 0) {
208
+ cleanup();
209
+ process.stdout.write('\r\n');
210
+ resolve('/exit');
211
+ return;
212
+ }
213
+ // ── Backspace ─────────────────────────────────────────────
214
+ if (s === '\x7f' || s === '\b') {
215
+ if (col > 0) {
216
+ const line = lines[row];
217
+ lines[row] = line.slice(0, col - 1) + line.slice(col);
218
+ col--;
219
+ process.stdout.write('\b \b');
220
+ if (col < lines[row].length)
221
+ redraw();
222
+ }
223
+ else if (row > 0) {
224
+ // Join with previous line
225
+ const prevLen = lines[row - 1].length;
226
+ lines[row - 1] += lines[row];
227
+ lines.splice(row, 1);
228
+ row--;
229
+ col = prevLen;
230
+ redraw();
231
+ }
232
+ return;
233
+ }
234
+ // ── Escape sequences (arrows, home, end, delete) ──────────
235
+ if (s.startsWith('\x1b[')) {
236
+ // Arrow keys: CSI n A/B/C/D
237
+ const m = s.match(/^\x1b\[(\d*)([ABCD])/);
238
+ if (m) {
239
+ const n = m[1] ? parseInt(m[1], 10) : 1;
240
+ const dir = m[2];
241
+ if (dir === 'D' && col > 0) {
242
+ col = Math.max(0, col - n);
243
+ process.stdout.write(`\x1b[${n}D`);
244
+ }
245
+ else if (dir === 'C' && col < lines[row].length) {
246
+ col = Math.min(lines[row].length, col + n);
247
+ process.stdout.write(`\x1b[${n}C`);
248
+ }
249
+ else if (dir === 'A' && row > 0) {
250
+ row = Math.max(0, row - n);
251
+ col = Math.min(col, lines[row].length);
252
+ process.stdout.write(`\x1b[${n}A`);
253
+ }
254
+ else if (dir === 'B' && row < lines.length - 1) {
255
+ row = Math.min(lines.length - 1, row + n);
256
+ col = Math.min(col, lines[row].length);
257
+ process.stdout.write(`\x1b[${n}B`);
258
+ }
259
+ return;
260
+ }
261
+ // Home
262
+ if (s === '\x1b[H' || s === '\x1b[1~') {
263
+ const diff = col;
264
+ col = 0;
265
+ process.stdout.write(`\x1b[${diff}D`);
266
+ return;
267
+ }
268
+ // End
269
+ if (s === '\x1b[F' || s === '\x1b[4~') {
270
+ const diff = lines[row].length - col;
271
+ col = lines[row].length;
272
+ process.stdout.write(`\x1b[${diff}C`);
273
+ return;
274
+ }
275
+ // Delete
276
+ if (s === '\x1b[3~') {
277
+ if (col < lines[row].length) {
278
+ lines[row] = lines[row].slice(0, col) + lines[row].slice(col + 1);
279
+ redraw();
280
+ }
281
+ else if (row < lines.length - 1) {
282
+ // Join with next line
283
+ lines[row] += lines[row + 1];
284
+ lines.splice(row + 1, 1);
285
+ redraw();
286
+ }
287
+ return;
288
+ }
289
+ // Unknown escape — ignore
290
+ return;
291
+ }
292
+ // ── Regular character ─────────────────────────────────────
293
+ const before = lines[row];
294
+ lines[row] = before.slice(0, col) + s + before.slice(col);
295
+ col += s.length;
296
+ if (col < lines[row].length) {
297
+ // Cursor is now in the middle of the line — need to repaint
298
+ redraw();
299
+ }
300
+ else {
301
+ // Cursor at end — simple echo suffices
302
+ process.stdout.write(s);
303
+ }
304
+ };
305
+ process.stdin.on('data', onData);
306
+ });
307
+ }
308
+ /**
309
+ * Create a prompt function appropriate for the current terminal.
310
+ * Returns multilinePrompt for TTYs, simplePrompt for pipes/files.
311
+ */
312
+ export function createPrompt() {
313
+ const isTTY = process.stdin.isTTY && typeof process.stdin.setRawMode === 'function';
314
+ return {
315
+ prompt: isTTY ? multilinePrompt : simplePrompt,
316
+ enableBracketedPaste: () => {
317
+ if (isTTY)
318
+ process.stdout.write('\x1b[?2004h');
319
+ },
320
+ disableBracketedPaste: () => {
321
+ if (isTTY)
322
+ process.stdout.write('\x1b[?2004l');
323
+ },
324
+ };
325
+ }
@@ -0,0 +1,29 @@
1
+ interface ChecksumEntry {
2
+ hash: string;
3
+ size: number;
4
+ scannedAt: number;
5
+ }
6
+ interface Checksums {
7
+ scannedAt: number;
8
+ gitHash: string | null;
9
+ files: Record<string, ChecksumEntry>;
10
+ }
11
+ export declare function cacheIsStale(workDir: string): Promise<boolean>;
12
+ /**
13
+ * Load stored checksums for quick file-change detection.
14
+ * The agent can use this to skip re-reading files that haven't changed.
15
+ */
16
+ export declare function loadChecksums(workDir: string): Promise<Checksums | null>;
17
+ /**
18
+ * Run the background scanner.
19
+ * Checks cache validity first; if stale, re-scans the codebase.
20
+ *
21
+ * @param workDir Working directory to scan
22
+ * @param force Force rescan even if cache is fresh
23
+ * @returns Scan result with file count
24
+ */
25
+ export declare function runScanner(workDir: string, force?: boolean): Promise<{
26
+ fileCount: number;
27
+ cached: boolean;
28
+ }>;
29
+ export {};